diff options
Diffstat (limited to 'program/include')
48 files changed, 385 insertions, 29188 deletions
diff --git a/program/include/rcube_bc.inc b/program/include/bc.php index 1894873e6..5047e0a84 100644 --- a/program/include/rcube_bc.inc +++ b/program/include/bc.php @@ -2,7 +2,7 @@ /* +-----------------------------------------------------------------------+ - | program/include/main.inc | + | program/include/bc.php | | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2005-2012, The Roundcube Dev Team | @@ -31,7 +31,8 @@ define('RCUBE_INPUT_GET', rcube_utils::INPUT_GET); define('RCUBE_INPUT_POST', rcube_utils::INPUT_POST); define('RCUBE_INPUT_GPC', rcube_utils::INPUT_GPC); -define('JS_OBJECT_NAME', rcmail::JS_OBJECT_NAME); +define('JS_OBJECT_NAME', rcmail_output::JS_OBJECT_NAME); +define('RCMAIL_CHARSET', RCUBE_CHARSET); function get_table_name($table) { @@ -83,19 +84,19 @@ function json_serialize($input) return rcube_output::json_serialize($input); } -function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE) +function rep_specialchars_output($str, $enctype='', $mode='', $newlines=true) { return rcube_utils::rep_specialchars_output($str, $enctype, $mode, $newlines); } -function Q($str, $mode='strict', $newlines=TRUE) +function Q($str, $mode='strict', $newlines=true) { - return rcmail::Q($str, $mode, $newlines); + return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines); } function JQ($str) { - return rcmail::JQ($str); + return rcube_utils::rep_specialchars_output($str, 'js'); } function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL) @@ -125,7 +126,7 @@ function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col) function rcmail_get_edit_field($col, $value, $attrib, $type='text') { - return rcube_utils::get_edit_field($col, $value, $attrib, $type); + return rcube_output::get_edit_field($col, $value, $attrib, $type); } function rcmail_mod_css_styles($source, $container_id, $allow_remote=false) @@ -338,9 +339,9 @@ function show_bytes($bytes) return rcmail::get_instance()->show_bytes($bytes); } -function rc_wordwrap($string, $width=75, $break="\n", $cut=false) +function rc_wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null) { - return rcube_mime::wordwrap($string, $width, $break, $cut); + return rcube_mime::wordwrap($string, $width, $break, $cut, $charset); } function rc_request_header($name) @@ -382,3 +383,23 @@ function send_future_expire_header($offset = 2600000) { return rcmail::get_instance()->output->future_expire_header($offset); } + +function get_opt($aliases = array()) +{ + return rcube_utils::get_opt($aliases); +} + +function prompt_silent($prompt = 'Password:') +{ + return rcube_utils::prompt_silent($prompt); +} + +function get_boolean($str) +{ + return rcube_utils::get_boolean($str); +} + +class rcube_html_page extends rcmail_html_page +{ + +} diff --git a/program/include/clisetup.php b/program/include/clisetup.php index a9af90a6f..07e318521 100644 --- a/program/include/clisetup.php +++ b/program/include/clisetup.php @@ -27,71 +27,3 @@ require_once INSTALL_PATH . 'program/include/iniset.php'; // Unset max. execution time limit, set to 120 seconds in iniset.php @set_time_limit(0); - -/** - * Parse commandline arguments into a hash array - */ -function get_opt($aliases = array()) -{ - $args = array(); - - for ($i=1; $i < count($_SERVER['argv']); $i++) { - $arg = $_SERVER['argv'][$i]; - $value = true; - $key = null; - - if ($arg[0] == '-') { - $key = preg_replace('/^-+/', '', $arg); - $sp = strpos($arg, '='); - if ($sp > 0) { - $key = substr($key, 0, $sp - 2); - $value = substr($arg, $sp+1); - } - else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') { - $value = $_SERVER['argv'][++$i]; - } - - $args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value; - } - else { - $args[] = $arg; - } - - if ($alias = $aliases[$key]) { - $args[$alias] = $args[$key]; - } - } - - return $args; -} - - -/** - * from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/ - */ -function prompt_silent($prompt = "Password:") -{ - if (preg_match('/^win/i', PHP_OS)) { - $vbscript = sys_get_temp_dir() . 'prompt_password.vbs'; - file_put_contents($vbscript, 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))'); - $command = "cscript //nologo " . escapeshellarg($vbscript); - $password = rtrim(shell_exec($command)); - unlink($vbscript); - return $password; - } - else { - $command = "/usr/bin/env bash -c 'echo OK'"; - if (rtrim(shell_exec($command)) !== 'OK') { - echo $prompt; - $pass = trim(fgets(STDIN)); - echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n"; - return $pass; - } - $command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'"; - $password = rtrim(shell_exec($command)); - echo "\n"; - return $password; - } -} - -?> diff --git a/program/include/html.php b/program/include/html.php deleted file mode 100644 index 880873ddc..000000000 --- a/program/include/html.php +++ /dev/null @@ -1,848 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/html.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2011, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Helper class to create valid XHTML code | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Class for HTML code creation - * - * @package HTML - */ -class html -{ - protected $tagname; - protected $attrib = array(); - protected $allowed = array(); - protected $content; - - public static $doctype = 'xhtml'; - public static $lc_tags = true; - public static $common_attrib = array('id','class','style','title','align'); - public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script'); - - /** - * Constructor - * - * @param array $attrib Hash array with tag attributes - */ - public function __construct($attrib = array()) - { - if (is_array($attrib)) { - $this->attrib = $attrib; - } - } - - /** - * Return the tag code - * - * @return string The finally composed HTML tag - */ - public function show() - { - return self::tag($this->tagname, $this->attrib, $this->content, array_merge(self::$common_attrib, $this->allowed)); - } - - /****** STATIC METHODS *******/ - - /** - * Generic method to create a HTML tag - * - * @param string $tagname Tag name - * @param array $attrib Tag attributes as key/value pairs - * @param string $content Optinal Tag content (creates a container tag) - * @param array $allowed_attrib List with allowed attributes, omit to allow all - * @return string The XHTML tag - */ - public static function tag($tagname, $attrib = array(), $content = null, $allowed_attrib = null) - { - if (is_string($attrib)) - $attrib = array('class' => $attrib); - - $inline_tags = array('a','span','img'); - $suffix = $attrib['nl'] || ($content && $attrib['nl'] !== false && !in_array($tagname, $inline_tags)) ? "\n" : ''; - - $tagname = self::$lc_tags ? strtolower($tagname) : $tagname; - if (isset($content) || in_array($tagname, self::$containers)) { - $suffix = $attrib['noclose'] ? $suffix : '</' . $tagname . '>' . $suffix; - unset($attrib['noclose'], $attrib['nl']); - return '<' . $tagname . self::attrib_string($attrib, $allowed_attrib) . '>' . $content . $suffix; - } - else { - return '<' . $tagname . self::attrib_string($attrib, $allowed_attrib) . '>' . $suffix; - } - } - - /** - * - */ - public static function doctype($type) - { - $doctypes = array( - 'html5' => '<!DOCTYPE html>', - 'xhtml' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', - 'xhtml-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', - 'xhtml-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', - ); - - if ($doctypes[$type]) { - self::$doctype = preg_replace('/-\w+$/', '', $type); - return $doctypes[$type]; - } - - return ''; - } - - /** - * Derrived method for <div> containers - * - * @param mixed $attr Hash array with tag attributes or string with class name - * @param string $cont Div content - * @return string HTML code - * @see html::tag() - */ - public static function div($attr = null, $cont = null) - { - if (is_string($attr)) { - $attr = array('class' => $attr); - } - return self::tag('div', $attr, $cont, array_merge(self::$common_attrib, array('onclick'))); - } - - /** - * Derrived method for <p> blocks - * - * @param mixed $attr Hash array with tag attributes or string with class name - * @param string $cont Paragraph content - * @return string HTML code - * @see html::tag() - */ - public static function p($attr = null, $cont = null) - { - if (is_string($attr)) { - $attr = array('class' => $attr); - } - return self::tag('p', $attr, $cont, self::$common_attrib); - } - - /** - * Derrived method to create <img /> - * - * @param mixed $attr Hash array with tag attributes or string with image source (src) - * @return string HTML code - * @see html::tag() - */ - public static function img($attr = null) - { - if (is_string($attr)) { - $attr = array('src' => $attr); - } - return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib, - array('src','alt','width','height','border','usemap','onclick'))); - } - - /** - * Derrived method for link tags - * - * @param mixed $attr Hash array with tag attributes or string with link location (href) - * @param string $cont Link content - * @return string HTML code - * @see html::tag() - */ - public static function a($attr, $cont) - { - if (is_string($attr)) { - $attr = array('href' => $attr); - } - return self::tag('a', $attr, $cont, array_merge(self::$common_attrib, - array('href','target','name','rel','onclick','onmouseover','onmouseout','onmousedown','onmouseup'))); - } - - /** - * Derrived method for inline span tags - * - * @param mixed $attr Hash array with tag attributes or string with class name - * @param string $cont Tag content - * @return string HTML code - * @see html::tag() - */ - public static function span($attr, $cont) - { - if (is_string($attr)) { - $attr = array('class' => $attr); - } - return self::tag('span', $attr, $cont, self::$common_attrib); - } - - /** - * Derrived method for form element labels - * - * @param mixed $attr Hash array with tag attributes or string with 'for' attrib - * @param string $cont Tag content - * @return string HTML code - * @see html::tag() - */ - public static function label($attr, $cont) - { - if (is_string($attr)) { - $attr = array('for' => $attr); - } - return self::tag('label', $attr, $cont, array_merge(self::$common_attrib, array('for'))); - } - - /** - * Derrived method to create <iframe></iframe> - * - * @param mixed $attr Hash array with tag attributes or string with frame source (src) - * @return string HTML code - * @see html::tag() - */ - public static function iframe($attr = null, $cont = null) - { - if (is_string($attr)) { - $attr = array('src' => $attr); - } - return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib, - array('src','name','width','height','border','frameborder'))); - } - - /** - * Derrived method to create <script> tags - * - * @param mixed $attr Hash array with tag attributes or string with script source (src) - * @param string $cont Javascript code to be placed as tag content - * @return string HTML code - * @see html::tag() - */ - public static function script($attr, $cont = null) - { - if (is_string($attr)) { - $attr = array('src' => $attr); - } - if ($cont) { - if (self::$doctype == 'xhtml') - $cont = "\n/* <![CDATA[ */\n" . $cont . "\n/* ]]> */\n"; - else - $cont = "\n" . $cont . "\n"; - } - - return self::tag('script', $attr + array('type' => 'text/javascript', 'nl' => true), - $cont, array_merge(self::$common_attrib, array('src','type','charset'))); - } - - /** - * Derrived method for line breaks - * - * @return string HTML code - * @see html::tag() - */ - public static function br() - { - return self::tag('br'); - } - - /** - * Create string with attributes - * - * @param array $attrib Associative arry with tag attributes - * @param array $allowed List of allowed attributes - * @return string Valid attribute string - */ - public static function attrib_string($attrib = array(), $allowed = null) - { - if (empty($attrib)) { - return ''; - } - - $allowed_f = array_flip((array)$allowed); - $attrib_arr = array(); - foreach ($attrib as $key => $value) { - // skip size if not numeric - if ($key == 'size' && !is_numeric($value)) { - continue; - } - - // ignore "internal" or not allowed attributes - if ($key == 'nl' || ($allowed && !isset($allowed_f[$key])) || $value === null) { - continue; - } - - // skip empty eventhandlers - if (preg_match('/^on[a-z]+/', $key) && !$value) { - continue; - } - - // attributes with no value - if (in_array($key, array('checked', 'multiple', 'disabled', 'selected'))) { - if ($value) { - $attrib_arr[] = $key . '="' . $key . '"'; - } - } - else { - $attrib_arr[] = $key . '="' . self::quote($value) . '"'; - } - } - - return count($attrib_arr) ? ' '.implode(' ', $attrib_arr) : ''; - } - - /** - * Convert a HTML attribute string attributes to an associative array (name => value) - * - * @param string Input string - * @return array Key-value pairs of parsed attributes - */ - public static function parse_attrib_string($str) - { - $attrib = array(); - $regexp = '/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui'; - - preg_match_all($regexp, stripslashes($str), $regs, PREG_SET_ORDER); - - // convert attributes to an associative array (name => value) - if ($regs) { - foreach ($regs as $attr) { - $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]); - } - } - - return $attrib; - } - - /** - * Replacing specials characters in html attribute value - * - * @param string $str Input string - * - * @return string The quoted string - */ - public static function quote($str) - { - return @htmlspecialchars($str, ENT_COMPAT, RCMAIL_CHARSET); - } -} - - -/** - * Class to create an HTML input field - * - * @package HTML - */ -class html_inputfield extends html -{ - protected $tagname = 'input'; - protected $type = 'text'; - protected $allowed = array( - 'type','name','value','size','tabindex','autocapitalize', - 'autocomplete','checked','onchange','onclick','disabled','readonly', - 'spellcheck','results','maxlength','src','multiple','placeholder', - ); - - /** - * Object constructor - * - * @param array $attrib Associative array with tag attributes - */ - public function __construct($attrib = array()) - { - if (is_array($attrib)) { - $this->attrib = $attrib; - } - - if ($attrib['type']) { - $this->type = $attrib['type']; - } - } - - /** - * Compose input tag - * - * @param string $value Field value - * @param array $attrib Additional attributes to override - * @return string HTML output - */ - public function show($value = null, $attrib = null) - { - // overwrite object attributes - if (is_array($attrib)) { - $this->attrib = array_merge($this->attrib, $attrib); - } - - // set value attribute - if ($value !== null) { - $this->attrib['value'] = $value; - } - // set type - $this->attrib['type'] = $this->type; - return parent::show(); - } -} - -/** - * Class to create an HTML password field - * - * @package HTML - */ -class html_passwordfield extends html_inputfield -{ - protected $type = 'password'; -} - -/** - * Class to create an hidden HTML input field - * - * @package HTML - */ - -class html_hiddenfield extends html -{ - protected $tagname = 'input'; - protected $type = 'hidden'; - protected $fields_arr = array(); - protected $allowed = array('type','name','value','onchange','disabled','readonly'); - - /** - * Constructor - * - * @param array $attrib Named tag attributes - */ - public function __construct($attrib = null) - { - if (is_array($attrib)) { - $this->add($attrib); - } - } - - /** - * Add a hidden field to this instance - * - * @param array $attrib Named tag attributes - */ - public function add($attrib) - { - $this->fields_arr[] = $attrib; - } - - /** - * Create HTML code for the hidden fields - * - * @return string Final HTML code - */ - public function show() - { - $out = ''; - foreach ($this->fields_arr as $attrib) { - $out .= self::tag($this->tagname, array('type' => $this->type) + $attrib); - } - return $out; - } -} - -/** - * Class to create HTML radio buttons - * - * @package HTML - */ -class html_radiobutton extends html_inputfield -{ - protected $type = 'radio'; - - /** - * Get HTML code for this object - * - * @param string $value Value of the checked field - * @param array $attrib Additional attributes to override - * @return string HTML output - */ - public function show($value = '', $attrib = null) - { - // overwrite object attributes - if (is_array($attrib)) { - $this->attrib = array_merge($this->attrib, $attrib); - } - - // set value attribute - $this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']); - - return parent::show(); - } -} - -/** - * Class to create HTML checkboxes - * - * @package HTML - */ -class html_checkbox extends html_inputfield -{ - protected $type = 'checkbox'; - - /** - * Get HTML code for this object - * - * @param string $value Value of the checked field - * @param array $attrib Additional attributes to override - * @return string HTML output - */ - public function show($value = '', $attrib = null) - { - // overwrite object attributes - if (is_array($attrib)) { - $this->attrib = array_merge($this->attrib, $attrib); - } - - // set value attribute - $this->attrib['checked'] = ((string)$value == (string)$this->attrib['value']); - - return parent::show(); - } -} - -/** - * Class to create an HTML textarea - * - * @package HTML - */ -class html_textarea extends html -{ - protected $tagname = 'textarea'; - protected $allowed = array('name','rows','cols','wrap','tabindex', - 'onchange','disabled','readonly','spellcheck'); - - /** - * Get HTML code for this object - * - * @param string $value Textbox value - * @param array $attrib Additional attributes to override - * @return string HTML output - */ - public function show($value = '', $attrib = null) - { - // overwrite object attributes - if (is_array($attrib)) { - $this->attrib = array_merge($this->attrib, $attrib); - } - - // take value attribute as content - if (empty($value) && !empty($this->attrib['value'])) { - $value = $this->attrib['value']; - } - - // make shure we don't print the value attribute - if (isset($this->attrib['value'])) { - unset($this->attrib['value']); - } - - if (!empty($value) && empty($this->attrib['is_escaped'])) { - $value = self::quote($value); - } - - return self::tag($this->tagname, $this->attrib, $value, - array_merge(self::$common_attrib, $this->allowed)); - } -} - -/** - * Builder for HTML drop-down menus - * Syntax:<pre> - * // create instance. arguments are used to set attributes of select-tag - * $select = new html_select(array('name' => 'fieldname')); - * - * // add one option - * $select->add('Switzerland', 'CH'); - * - * // add multiple options - * $select->add(array('Switzerland','Germany'), array('CH','DE')); - * - * // generate pulldown with selection 'Switzerland' and return html-code - * // as second argument the same attributes available to instanciate can be used - * print $select->show('CH'); - * </pre> - * - * @package HTML - */ -class html_select extends html -{ - protected $tagname = 'select'; - protected $options = array(); - protected $allowed = array('name','size','tabindex','autocomplete', - 'multiple','onchange','disabled','rel'); - - /** - * Add a new option to this drop-down - * - * @param mixed $names Option name or array with option names - * @param mixed $values Option value or array with option values - */ - public function add($names, $values = null) - { - if (is_array($names)) { - foreach ($names as $i => $text) { - $this->options[] = array('text' => $text, 'value' => $values[$i]); - } - } - else { - $this->options[] = array('text' => $names, 'value' => $values); - } - } - - /** - * Get HTML code for this object - * - * @param string $select Value of the selection option - * @param array $attrib Additional attributes to override - * @return string HTML output - */ - public function show($select = array(), $attrib = null) - { - // overwrite object attributes - if (is_array($attrib)) { - $this->attrib = array_merge($this->attrib, $attrib); - } - - $this->content = "\n"; - $select = (array)$select; - foreach ($this->options as $option) { - $attr = array( - 'value' => $option['value'], - 'selected' => (in_array($option['value'], $select, true) || - in_array($option['text'], $select, true)) ? 1 : null); - - $option_content = $option['text']; - if (empty($this->attrib['is_escaped'])) { - $option_content = self::quote($option_content); - } - - $this->content .= self::tag('option', $attr, $option_content); - } - - return parent::show(); - } -} - - -/** - * Class to build an HTML table - * - * @package HTML - */ -class html_table extends html -{ - protected $tagname = 'table'; - protected $allowed = array('id','class','style','width','summary', - 'cellpadding','cellspacing','border'); - - private $header = array(); - private $rows = array(); - private $rowindex = 0; - private $colindex = 0; - - /** - * Constructor - * - * @param array $attrib Named tag attributes - */ - public function __construct($attrib = array()) - { - $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array(); - $this->attrib = array_merge($attrib, $default_attrib); - } - - /** - * Add a table cell - * - * @param array $attr Cell attributes - * @param string $cont Cell content - */ - public function add($attr, $cont) - { - if (is_string($attr)) { - $attr = array('class' => $attr); - } - - $cell = new stdClass; - $cell->attrib = $attr; - $cell->content = $cont; - - $this->rows[$this->rowindex]->cells[$this->colindex] = $cell; - $this->colindex += max(1, intval($attr['colspan'])); - - if ($this->attrib['cols'] && $this->colindex >= $this->attrib['cols']) { - $this->add_row(); - } - } - - /** - * Add a table header cell - * - * @param array $attr Cell attributes - * @param string $cont Cell content - */ - public function add_header($attr, $cont) - { - if (is_string($attr)) { - $attr = array('class' => $attr); - } - - $cell = new stdClass; - $cell->attrib = $attr; - $cell->content = $cont; - $this->header[] = $cell; - } - - /** - * Remove a column from a table - * Useful for plugins making alterations - * - * @param string $class - */ - public function remove_column($class) - { - // Remove the header - foreach ($this->header as $index=>$header){ - if ($header->attrib['class'] == $class){ - unset($this->header[$index]); - break; - } - } - - // Remove cells from rows - foreach ($this->rows as $i=>$row){ - foreach ($row->cells as $j=>$cell){ - if ($cell->attrib['class'] == $class){ - unset($this->rows[$i]->cells[$j]); - break; - } - } - } - } - - /** - * Jump to next row - * - * @param array $attr Row attributes - */ - public function add_row($attr = array()) - { - $this->rowindex++; - $this->colindex = 0; - $this->rows[$this->rowindex] = new stdClass; - $this->rows[$this->rowindex]->attrib = $attr; - $this->rows[$this->rowindex]->cells = array(); - } - - /** - * Set row attributes - * - * @param array $attr Row attributes - * @param int $index Optional row index (default current row index) - */ - public function set_row_attribs($attr = array(), $index = null) - { - if (is_string($attr)) { - $attr = array('class' => $attr); - } - - if ($index === null) { - $index = $this->rowindex; - } - - $this->rows[$index]->attrib = $attr; - } - - /** - * Get row attributes - * - * @param int $index Row index - * - * @return array Row attributes - */ - public function get_row_attribs($index = null) - { - if ($index === null) { - $index = $this->rowindex; - } - - return $this->rows[$index] ? $this->rows[$index]->attrib : null; - } - - /** - * Build HTML output of the table data - * - * @param array $attrib Table attributes - * @return string The final table HTML code - */ - public function show($attrib = null) - { - if (is_array($attrib)) - $this->attrib = array_merge($this->attrib, $attrib); - - $thead = $tbody = ""; - - // include <thead> - if (!empty($this->header)) { - $rowcontent = ''; - foreach ($this->header as $c => $col) { - $rowcontent .= self::tag('td', $col->attrib, $col->content); - } - $thead = self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)); - } - - foreach ($this->rows as $r => $row) { - $rowcontent = ''; - foreach ($row->cells as $c => $col) { - $rowcontent .= self::tag('td', $col->attrib, $col->content); - } - - if ($r < $this->rowindex || count($row->cells)) { - $tbody .= self::tag('tr', $row->attrib, $rowcontent, parent::$common_attrib); - } - } - - if ($this->attrib['rowsonly']) { - return $tbody; - } - - // add <tbody> - $this->content = $thead . self::tag('tbody', null, $tbody); - - unset($this->attrib['cols'], $this->attrib['rowsonly']); - return parent::show(); - } - - /** - * Count number of rows - * - * @return The number of rows - */ - public function size() - { - return count($this->rows); - } - - /** - * Remove table body (all rows) - */ - public function remove_body() - { - $this->rows = array(); - $this->rowindex = 0; - } - -} diff --git a/program/include/iniset.php b/program/include/iniset.php index 82278c9f8..be71fc084 100644 --- a/program/include/iniset.php +++ b/program/include/iniset.php @@ -12,7 +12,7 @@ | See the README file for a full license statement. | | | | PURPOSE: | - | Setup the application envoronment required to process | + | Setup the application environment required to process | | any request. | +-----------------------------------------------------------------------+ | Author: Till Klampaeckel <till@php.net> | @@ -20,31 +20,25 @@ +-----------------------------------------------------------------------+ */ +// application constants +define('RCMAIL_VERSION', '0.9-git'); +define('RCMAIL_START', microtime(true)); + $config = array( - 'error_reporting' => E_ALL &~ (E_NOTICE | E_STRICT), // Some users are not using Installer, so we'll check some // critical PHP settings here. Only these, which doesn't provide // an error/warning in the logs later. See (#1486307). - 'mbstring.func_overload' => 0, 'suhosin.session.encrypt' => 0, 'session.auto_start' => 0, 'file_uploads' => 1, - 'magic_quotes_runtime' => 0, - 'magic_quotes_sybase' => 0, // #1488506 ); foreach ($config as $optname => $optval) { if ($optval != ini_get($optname) && @ini_set($optname, $optval) === false) { die("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).\n" - ."Read REQUIREMENTS section in INSTALL file or use Roundcube Installer!"); + ."Check your PHP configuration (including php_admin_flag)."); } } -// application constants -define('RCMAIL_VERSION', '0.9-git'); -define('RCMAIL_CHARSET', 'UTF-8'); -define('RCMAIL_START', microtime(true)); - if (!defined('INSTALL_PATH')) { define('INSTALL_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/'); } @@ -53,6 +47,14 @@ if (!defined('RCMAIL_CONFIG_DIR')) { define('RCMAIL_CONFIG_DIR', INSTALL_PATH . 'config'); } +if (!defined('RCUBE_LOCALIZATION_DIR')) { + define('RCUBE_LOCALIZATION_DIR', INSTALL_PATH . 'program/localization/'); +} + +define('RCUBE_INSTALL_PATH', INSTALL_PATH); +define('RCUBE_CONFIG_DIR', RCMAIL_CONFIG_DIR.'/'); + + // RC include folders MUST be included FIRST to avoid other // possible not compatible libraries (i.e PEAR) to be included // instead the ones provided by RC @@ -67,20 +69,29 @@ if (set_include_path($include_path) === false) { // (does not work in safe mode) @set_time_limit(120); -// set internal encoding for mbstring extension -if (extension_loaded('mbstring')) { - mb_internal_encoding(RCMAIL_CHARSET); - @mb_regex_encoding(RCMAIL_CHARSET); -} +// include Roundcube Framework +require_once 'Roundcube/bootstrap.php'; -// include global functions -require_once INSTALL_PATH . 'program/include/rcube_shared.inc'; +// register autoloader for rcmail app classes +spl_autoload_register('rcmail_autoload'); -// Register autoloader -spl_autoload_register('rcube_autoload'); +// backward compatybility (to be removed) +require_once INSTALL_PATH . 'program/include/bc.php'; -// set PEAR error handling (will also load the PEAR main class) -PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'rcube_pear_error'); -// backward compatybility (to be removed) -require_once INSTALL_PATH . 'program/include/rcube_bc.inc'; +/** + * PHP5 autoloader routine for dynamic class loading + */ +function rcmail_autoload($classname) +{ + if (strpos($classname, 'rcmail') === 0) { + $filepath = INSTALL_PATH . "program/include/$classname.php"; + if (is_readable($filepath)) { + include_once $filepath; + return true; + } + } + + return false; +} + diff --git a/program/include/rcmail.php b/program/include/rcmail.php index c2f76b388..8e01a2155 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -56,8 +56,6 @@ class rcmail extends rcube private $action_map = array(); - const JS_OBJECT_NAME = 'rcmail'; - const ERROR_STORAGE = -2; const ERROR_INVALID_REQUEST = 1; const ERROR_INVALID_HOST = 2; @@ -94,9 +92,6 @@ class rcmail extends rcube // create user object $this->set_user(new rcube_user($_SESSION['user_id'])); - // configure session (after user config merge!) - $this->session_configure(); - // set task and action properties $this->set_task(rcube_utils::get_input_value('_task', rcube_utils::INPUT_GPC)); $this->action = asciiwords(rcube_utils::get_input_value('_action', rcube_utils::INPUT_GPC)); @@ -210,14 +205,22 @@ class rcmail extends rcube } } + // when user requested default writeable addressbook + // we need to check if default is writeable, if not we + // will return first writeable book (if any exist) + if ($contacts && $default && $contacts->readonly && $writeable) { + $contacts = null; + } + // Get first addressbook from the list if configured default doesn't exist // This can happen when user deleted the addressbook (e.g. Kolab folder) if (!$contacts && (!$id || $default)) { - $source = reset($this->get_address_sources($writeable)); + $source = reset($this->get_address_sources($writeable, !$default)); if (!empty($source)) { $contacts = $this->get_address_book($source['id']); - if ($contacts) + if ($contacts) { $id = $source['id']; + } } } @@ -229,16 +232,17 @@ class rcmail extends rcube true, true); } + // add to the 'books' array for shutdown function + $this->address_books[$id] = $contacts; + if ($writeable && $contacts->readonly) { return null; } // set configured sort order - if ($sort_col = $this->config->get('addressbook_sort_col')) + if ($sort_col = $this->config->get('addressbook_sort_col')) { $contacts->set_sort_order($sort_col); - - // add to the 'books' array for shutdown function - $this->address_books[$id] = $contacts; + } return $contacts; } @@ -248,10 +252,11 @@ class rcmail extends rcube * Return address books list * * @param boolean True if the address book needs to be writeable + * @param boolean True if the address book needs to be not hidden * * @return array Address books array */ - public function get_address_sources($writeable = false) + public function get_address_sources($writeable = false, $skip_hidden = false) { $abook_type = strtolower($this->config->get('address_book_type')); $ldap_config = $this->config->get('ldap_public'); @@ -295,11 +300,17 @@ class rcmail extends rcube foreach ($list as $idx => $item) { // register source for shutdown function - if (!is_object($this->address_books[$item['id']])) + if (!is_object($this->address_books[$item['id']])) { $this->address_books[$item['id']] = $item; + } // remove from list if not writeable as requested - if ($writeable && $item['readonly']) + if ($writeable && $item['readonly']) { unset($list[$idx]); + } + // remove from list if hidden as requested + else if ($skip_hidden && $item['hidden']) { + unset($list[$idx]); + } } return $list; @@ -308,22 +319,21 @@ class rcmail extends rcube /** * Init output object for GUI and add common scripts. - * This will instantiate a rcube_output_html object and set + * This will instantiate a rcmail_output_html object and set * environment vars according to the current session and configuration * * @param boolean True if this request is loaded in a (i)frame - * @return rcube_output_html Reference to HTML output object + * @return rcube_output Reference to HTML output object */ public function load_gui($framed = false) { // init output page - if (!($this->output instanceof rcube_output_html)) - $this->output = new rcube_output_html($this->task, $framed); + if (!($this->output instanceof rcmail_output_html)) + $this->output = new rcmail_output_html($this->task, $framed); - // set keep-alive/check-recent interval - if ($this->session && ($keep_alive = $this->session->get_keep_alive())) { - $this->output->set_env('keep_alive', $keep_alive); - } + // set refresh interval + $this->output->set_env('refresh_interval', $this->config->get('refresh_interval', 0)); + $this->output->set_env('session_lifetime', $this->config->get('session_lifetime', 0) * 60); if ($framed) { $this->comm_path .= '&_framed=1'; @@ -333,10 +343,10 @@ class rcmail extends rcube $this->output->set_env('task', $this->task); $this->output->set_env('action', $this->action); $this->output->set_env('comm_path', $this->comm_path); - $this->output->set_charset(RCMAIL_CHARSET); + $this->output->set_charset(RCUBE_CHARSET); // add some basic labels to client - $this->output->add_label('loading', 'servererror', 'requesttimedout'); + $this->output->add_label('loading', 'servererror', 'requesttimedout', 'refreshing'); return $this->output; } @@ -345,12 +355,12 @@ class rcmail extends rcube /** * Create an output object for JSON responses * - * @return rcube_output_json Reference to JSON output object + * @return rcube_output Reference to JSON output object */ public function json_init() { - if (!($this->output instanceof rcube_output_json)) - $this->output = new rcube_output_json($this->task); + if (!($this->output instanceof rcmail_output_json)) + $this->output = new rcmail_output_json($this->task); return $this->output; } @@ -522,7 +532,6 @@ class rcmail extends rcube // Configure environment $this->set_user($user); $this->set_storage_prop(); - $this->session_configure(); // fix some old settings according to namespace prefix $this->fix_namespace_settings($user); @@ -542,9 +551,7 @@ class rcmail extends rcube $_SESSION['login_time'] = time(); if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_') - $_SESSION['timezone'] = floatval($_REQUEST['_timezone']); - if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_') - $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']); + $_SESSION['timezone'] = rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_GPC); // force reloading complete list of subscribed mailboxes $storage->clear_cache('mailboxes', true); @@ -777,6 +784,7 @@ class rcmail extends rcube } } + /** * Registers action aliases for current task * @@ -791,6 +799,7 @@ class rcmail extends rcube } } + /** * Returns current action filename * @@ -805,6 +814,7 @@ class rcmail extends rcube return strtr($this->action, '-', '_') . '.inc'; } + /** * Fixes some user preferences according to namespace handling change. * Old Roundcube versions were using folder names with removed namespace prefix. @@ -1554,7 +1564,7 @@ class rcmail extends rcube $html_name = $this->Q($foldername) . ($unread ? html::span('unreadcount', sprintf($attrib['unreadwrap'], $unread)) : ''); $link_attrib = $folder['virtual'] ? array() : array( 'href' => $this->url(array('_mbox' => $folder['id'])), - 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail::JS_OBJECT_NAME, $js_name), + 'onclick' => sprintf("return %s.command('list','%s',this)", rcmail_output::JS_OBJECT_NAME, $js_name), 'rel' => $folder['id'], 'title' => $title, ); @@ -1567,7 +1577,7 @@ class rcmail extends rcube (!empty($folder['folders']) ? html::div(array( 'class' => ($is_collapsed ? 'collapsed' : 'expanded'), 'style' => "position:absolute", - 'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail::JS_OBJECT_NAME, $js_name) + 'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail_output::JS_OBJECT_NAME, $js_name) ), ' ') : '')); $jslist[$folder_id] = array( @@ -2017,30 +2027,6 @@ class rcmail extends rcube /** - * Quote a given string. - * Shortcut function for rcube_utils::rep_specialchars_output() - * - * @return string HTML-quoted string - */ - public static function Q($str, $mode = 'strict', $newlines = true) - { - return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines); - } - - - /** - * Quote a given string for javascript output. - * Shortcut function for rcube_utils::rep_specialchars_output() - * - * @return string JS-quoted string - */ - public static function JQ($str) - { - return rcube_utils::rep_specialchars_output($str, 'js'); - } - - - /** * Returns real size (calculated) of the message part * * @param rcube_message_part Message part diff --git a/program/include/rcube_result_set.php b/program/include/rcmail_html_page.php index 809d8743f..2624d590a 100644 --- a/program/include/rcube_result_set.php +++ b/program/include/rcmail_html_page.php @@ -2,17 +2,17 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_result_set.php | + | program/include/rcmail_html_page.php | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2011, The Roundcube Dev Team | + | Copyright (C) 2006-2012, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | | See the README file for a full license statement. | | | | PURPOSE: | - | Class representing an address directory result set | + | Render a simple HTML page with the given contents | | | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli <roundcube@gmail.com> | @@ -21,51 +21,16 @@ /** - * Roundcube result set class. - * Representing an address directory result set. + * Class to create HTML page output using a skin template * - * @package Addressbook + * @package Core + * @subpackage View */ -class rcube_result_set +class rcmail_html_page extends rcmail_output_html { - var $count = 0; - var $first = 0; - var $current = 0; - var $searchonly = false; - var $records = array(); - - - function __construct($c=0, $f=0) - { - $this->count = (int)$c; - $this->first = (int)$f; - } - - function add($rec) - { - $this->records[] = $rec; - } - - function iterate() - { - return $this->records[$this->current++]; - } - - function first() - { - $this->current = 0; - return $this->records[$this->current++]; - } - - // alias for iterate() - function next() + public function write($contents = '') { - return $this->iterate(); + self::reset(); + parent::write($contents); } - - function seek($i) - { - $this->current = $i; - } - -} +}
\ No newline at end of file diff --git a/program/include/rcmail_output.php b/program/include/rcmail_output.php new file mode 100644 index 000000000..36512ad48 --- /dev/null +++ b/program/include/rcmail_output.php @@ -0,0 +1,120 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcmail_output.php | + | | + | This file is part of the Roundcube PHP suite | + | Copyright (C) 2005-2012 The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | CONTENTS: | + | Abstract class for output generation | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + | Author: Aleksander Machniak <alec@alec.pl> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Class for output generation + * + * @package Core + * @subpackage View + */ +abstract class rcmail_output extends rcube_output +{ + const JS_OBJECT_NAME = 'rcmail'; + + public $type = 'html'; + public $ajax_call = false; + public $framed = false; + + protected $pagetitle = ''; + protected $object_handlers = array(); + + + /** + * Object constructor + */ + public function __construct($task = null, $framed = false) + { + parent::__construct(); + } + + + /** + * Setter for page title + * + * @param string $title Page title + */ + public function set_pagetitle($title) + { + $this->pagetitle = $title; + } + + + /** + * Getter for the current skin path property + */ + public function get_skin_path() + { + return $this->config->get('skin_path'); + } + + + /** + * Delete all stored env variables and commands + */ + public function reset() + { + parent::reset(); + + $this->object_handlers = array(); + $this->pagetitle = ''; + } + + + /** + * Call a client method + * + * @param string Method to call + * @param ... Additional arguments + */ + abstract function command(); + + + /** + * Add a localized label to the client environment + */ + abstract function add_label(); + + + /** + * Register a template object handler + * + * @param string Object name + * @param string Function name to call + * @return void + */ + public function add_handler($obj, $func) + { + $this->object_handlers[$obj] = $func; + } + + + /** + * Register a list of template object handlers + * + * @param array Hash array with object=>handler pairs + * @return void + */ + public function add_handlers($arr) + { + $this->object_handlers = array_merge($this->object_handlers, $arr); + } + +} diff --git a/program/include/rcube_output_html.php b/program/include/rcmail_output_html.php index d42171869..1290e173e 100644 --- a/program/include/rcube_output_html.php +++ b/program/include/rcmail_output_html.php @@ -2,7 +2,7 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcubeoutput_html.php | + | program/include/rcmail_output_html.php | | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2006-2012, The Roundcube Dev Team | @@ -23,9 +23,10 @@ /** * Class to create HTML page output using a skin template * - * @package View + * @package Core + * @subpackage View */ -class rcube_output_html extends rcube_output +class rcmail_output_html extends rcmail_output { public $type = 'html'; @@ -81,10 +82,10 @@ class rcube_output_html extends rcube_output $this->set_env('extwin', 1); // add common javascripts - $this->add_script('var '.rcmail::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top'); + $this->add_script('var '.self::JS_OBJECT_NAME.' = new rcube_webmail();', 'head_top'); // don't wait for page onload. Call init at the bottom of the page (delayed) - $this->add_script(rcmail::JS_OBJECT_NAME.'.init();', 'docready'); + $this->add_script(self::JS_OBJECT_NAME.'.init();', 'docready'); $this->scripts_path = 'program/js/'; $this->include_script('jquery.min.js'); @@ -206,6 +207,31 @@ class rcube_output_html extends rcube_output /** + * Find the given file in the current skin path stack + * + * @param string File name/path to resolve (starting with /) + * @param string Reference to the base path of the matching skin + * @param string Additional path to search in + * @return mixed Relative path to the requested file or False if not found + */ + public function get_skin_file($file, &$skin_path, $add_path = null) + { + $skin_paths = $this->skin_paths; + if ($add_path) + array_unshift($skin_paths, $add_path); + + foreach ($skin_paths as $skin_path) { + $path = realpath($skin_path . $file); + if (is_file($path)) { + return $skin_path . $file; + } + } + + return false; + } + + + /** * Register a GUI object to the client script * * @param string Object name @@ -214,7 +240,7 @@ class rcube_output_html extends rcube_output */ public function add_gui_object($obj, $id) { - $this->add_script(rcmail::JS_OBJECT_NAME.".gui_object('$obj', '$id');"); + $this->add_script(self::JS_OBJECT_NAME.".gui_object('$obj', '$id');"); } @@ -401,13 +427,13 @@ class rcube_output_html extends rcube_output // apply skin search escalation list to plugin directory $plugin_skin_paths = array(); foreach ($this->skin_paths as $skin_path) { - $plugin_skin_paths[] = $this->app->plugins->dir . $plugin . '/' . $skin_path; + $plugin_skin_paths[] = $this->app->plugins->url . $plugin . '/' . $skin_path; } // add fallback to default skin if (is_dir($this->app->plugins->dir . $plugin . '/skins/default')) { $skin_dir = $plugin . '/skins/default'; - $plugin_skin_paths[] = $this->app->plugins->dir . $skin_dir; + $plugin_skin_paths[] = $this->app->plugins->url . $skin_dir; } // add plugin skin paths to search list @@ -422,17 +448,20 @@ class rcube_output_html extends rcube_output // fallback to deprecated template names if (!is_readable($path) && $this->deprecated_templates[$realname]) { $path = "$skin_path/templates/" . $this->deprecated_templates[$realname] . ".html"; - rcube::raise_error(array( - 'code' => 502, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Using deprecated template '" . $this->deprecated_templates[$realname] - . "' in $skin_path/templates. Please rename to '$realname'"), - true, false); + + if (is_readable($path)) { + rcube::raise_error(array( + 'code' => 502, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Using deprecated template '" . $this->deprecated_templates[$realname] + . "' in $skin_path/templates. Please rename to '$realname'"), + true, false); + } } if (is_readable($path)) { $this->config->set('skin_path', $skin_path); - $this->base_path = $skin_path; + $this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path); // set base_path to core skin directory (not plugin's skin) break; } else { @@ -469,8 +498,6 @@ class rcube_output_html extends rcube_output $output = $hook['content']; unset($hook['content']); - $output = $this->parse_with_globals($this->fix_paths($output)); - // make sure all <form> tags have a valid request token $output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output); $this->footer = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $this->footer); @@ -509,7 +536,7 @@ class rcube_output_html extends rcube_output { $out = ''; if (!$this->framed && !empty($this->js_env)) { - $out .= rcmail::JS_OBJECT_NAME . '.set_env('.self::json_serialize($this->js_env).");\n"; + $out .= self::JS_OBJECT_NAME . '.set_env('.self::json_serialize($this->js_env).");\n"; } if (!empty($this->js_labels)) { $this->command('add_label', $this->js_labels); @@ -522,7 +549,7 @@ class rcube_output_html extends rcube_output $parent = $this->framed || preg_match('/^parent\./', $method); $out .= sprintf( "%s.%s(%s);\n", - ($parent ? 'if(window.parent && parent.'.rcmail::JS_OBJECT_NAME.') parent.' : '') . rcmail::JS_OBJECT_NAME, + ($parent ? 'if(window.parent && parent.'.self::JS_OBJECT_NAME.') parent.' : '') . self::JS_OBJECT_NAME, preg_replace('/^parent\./', '', $method), implode(',', $args) ); @@ -536,12 +563,17 @@ class rcube_output_html extends rcube_output * Make URLs starting with a slash point to skin directory * * @param string Input string + * @param boolean True if URL should be resolved using the current skin path stack * @return string */ - public function abs_url($str) + public function abs_url($str, $search_path = false) { - if ($str[0] == '/') + if ($str[0] == '/') { + if ($search_path && ($file_url = $this->get_skin_file($str, $skin_path))) + return $file_url; + return $this->base_path . $str; + } else return $str; } @@ -560,7 +592,7 @@ class rcube_output_html extends rcube_output $ERROR_CODE = $code; $ERROR_MESSAGE = $message; - include INSTALL_PATH . 'program/steps/utils/error.inc'; + include RCUBE_INSTALL_PATH . 'program/steps/utils/error.inc'; exit; } @@ -731,7 +763,7 @@ class rcube_output_html extends rcube_output ), array( "\$_SESSION['\\1']", - "\$this->app->config->get('\\1',get_boolean('\\3'))", + "\$this->app->config->get('\\1',rcube_utils::get_boolean('\\3'))", "\$this->env['\\1']", "rcube_utils::get_input_value('\\1', rcube_utils::INPUT_GPC)", "\$_COOKIE['\\1']", @@ -803,7 +835,7 @@ class rcube_output_html extends rcube_output unset($vars['name'], $vars['command']); $label = $this->app->gettext($attrib + array('vars' => $vars)); - $quoting = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (get_boolean((string)$attrib['html']) ? 'no' : ''); + $quoting = !empty($attrib['quoting']) ? strtolower($attrib['quoting']) : (rcube_utils::get_boolean((string)$attrib['html']) ? 'no' : ''); switch ($quoting) { case 'no': @@ -811,7 +843,7 @@ class rcube_output_html extends rcube_output break; case 'javascript': case 'js': - $label = rcmail::JQ($label); + $label = rcube::JQ($label); break; default: $label = html::quote($label); @@ -825,15 +857,9 @@ class rcube_output_html extends rcube_output // include a file case 'include': $old_base_path = $this->base_path; - $skin_paths = $this->skin_paths; - if ($attrib['skin_path']) - array_unshift($skin_paths, $attrib['skin_path']); - foreach ($skin_paths as $skin_path) { - $path = realpath($skin_path . $attrib['file']); - if (is_file($path)) { - $this->base_path = $skin_path; - break; - } + if ($path = $this->get_skin_file($attrib['file'], $skin_path, $attrib['skinpath'])) { + $this->base_path = preg_replace('!plugins/\w+/!', '', $skin_path); // set base_path to core skin directory (not plugin's skin) + $path = realpath($path); } if (is_readable($path)) { @@ -895,11 +921,11 @@ class rcube_output_html extends rcube_output } else if ($object == 'version') { $ver = (string)RCMAIL_VERSION; - if (is_file(INSTALL_PATH . '.svn/entries')) { + if (is_file(RCUBE_INSTALL_PATH . '.svn/entries')) { if (preg_match('/Revision:\s(\d+)/', @shell_exec('svn info'), $regs)) $ver .= ' [SVN r'.$regs[1].']'; } - else if (is_file(INSTALL_PATH . '.git/index')) { + else if (is_file(RCUBE_INSTALL_PATH . '.git/index')) { if (preg_match('/Date:\s+([^\n]+)/', @shell_exec('git log -1'), $regs)) { if ($date = date('Ymd.Hi', strtotime($regs[1]))) { $ver .= ' [GIT '.$date.']'; @@ -1053,7 +1079,7 @@ class rcube_output_html extends rcube_output if ($attrib['command']) { $this->add_script(sprintf( "%s.register_button('%s', '%s', '%s', '%s', '%s', '%s');", - rcmail::JS_OBJECT_NAME, + self::JS_OBJECT_NAME, $command, $attrib['id'], $attrib['type'], @@ -1065,7 +1091,7 @@ class rcube_output_html extends rcube_output // make valid href to specific buttons if (in_array($attrib['command'], rcmail::$main_tasks)) { $attrib['href'] = $this->app->url(array('task' => $attrib['command'])); - $attrib['onclick'] = sprintf("%s.command('switch-task','%s',null,event); return false", rcmail::JS_OBJECT_NAME, $attrib['command']); + $attrib['onclick'] = sprintf("return %s.command('switch-task','%s',this,event)", self::JS_OBJECT_NAME, $attrib['command']); } else if ($attrib['task'] && in_array($attrib['task'], rcmail::$main_tasks)) { $attrib['href'] = $this->app->url(array('action' => $attrib['command'], 'task' => $attrib['task'])); @@ -1089,7 +1115,7 @@ class rcube_output_html extends rcube_output else if ($command && !$attrib['onclick']) { $attrib['onclick'] = sprintf( "return %s.command('%s','%s',this,event)", - rcmail::JS_OBJECT_NAME, + self::JS_OBJECT_NAME, $command, $attrib['prop'] ); @@ -1333,11 +1359,13 @@ class rcube_output_html extends rcube_output $output = substr_replace($output, $css, $pos, 0); } + $output = $this->parse_with_globals($this->fix_paths($output)); + // trigger hook with final HTML content to be sent $hook = $this->app->plugins->exec_hook("send_page", array('content' => $output)); if (!$hook['abort']) { - if ($this->charset != RCMAIL_CHARSET) { - echo rcube_charset::convert($hook['content'], RCMAIL_CHARSET, $this->charset); + if ($this->charset != RCUBE_CHARSET) { + echo rcube_charset::convert($hook['content'], RCUBE_CHARSET, $this->charset); } else { echo $hook['content']; @@ -1350,21 +1378,25 @@ class rcube_output_html extends rcube_output * Returns iframe object, registers some related env variables * * @param array $attrib HTML attributes - * + * @param boolean $is_contentframe Register this iframe as the 'contentframe' gui object * @return string IFRAME element */ - public function frame($attrib) + public function frame($attrib, $is_contentframe = false) { + static $idcount = 0; + if (!$attrib['id']) { - $attrib['id'] = 'rcmframe'; + $attrib['id'] = 'rcmframe' . ++$idcount; } - if (!$attrib['name']) { - $attrib['name'] = $attrib['id']; - } + $attrib['name'] = $attrib['id']; + $attrib['src'] = $attrib['src'] ? $this->abs_url($attrib['src'], true) : 'program/resources/blank.gif'; - $this->set_env('contentframe', $attrib['id']); - $this->set_env('blankpage', $attrib['src'] ? $this->abs_url($attrib['src']) : 'program/resources/blank.gif'); + // register as 'contentframe' object + if ($is_contentframe || $attrib['contentframe']) { + $this->set_env('contentframe', $attrib['contentframe'] ? $attrib['contentframe'] : $attrib['name']); + $this->set_env('blankpage', $attrib['src']); + } return html::iframe($attrib); } @@ -1493,7 +1525,6 @@ class rcube_output_html extends rcube_output $input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login')); $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'login')); $input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => '_default_')); - $input_dst = new html_hiddenfield(array('name' => '_dstactive', 'id' => 'rcmlogindst', 'value' => '_default_')); $input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => $url)); $input_user = new html_inputfield(array('name' => '_user', 'id' => 'rcmloginuser') + $attrib + $user_attrib); @@ -1545,7 +1576,6 @@ class rcube_output_html extends rcube_output $out = $input_task->show(); $out .= $input_action->show(); $out .= $input_tzone->show(); - $out .= $input_dst->show(); $out .= $input_url->show(); $out .= $table->show(); @@ -1558,6 +1588,9 @@ class rcube_output_html extends rcube_output $out = $this->form_tag(array('name' => $form_name, 'method' => 'post'), $out); } + // include script for timezone detection + $this->include_script('jstz.min.js'); + return $out; } @@ -1615,7 +1648,7 @@ class rcube_output_html extends rcube_output if (empty($attrib['form'])) { $out = $this->form_tag(array( 'name' => "rcmqsearchform", - 'onsubmit' => rcmail::JS_OBJECT_NAME . ".command('search'); return false", + 'onsubmit' => self::JS_OBJECT_NAME . ".command('search'); return false", 'style' => "display:inline"), $out); } @@ -1727,7 +1760,7 @@ class rcube_output_html extends rcube_output 'about.html', ); foreach ($filenames as $file) { - $fn = RCMAIL_CONFIG_DIR . '/' . $file; + $fn = RCUBE_CONFIG_DIR . $file; if (is_readable($fn)) { $content = file_get_contents($fn); $content = $this->parse_conditions($content); diff --git a/program/include/rcube_output_json.php b/program/include/rcmail_output_json.php index eb1a9380d..def6ee42c 100644 --- a/program/include/rcube_output_json.php +++ b/program/include/rcmail_output_json.php @@ -2,7 +2,7 @@ /* +-----------------------------------------------------------------------+ - | program/include/rcube_output_json.php | + | program/include/rcmail_output_json.php | | | | This file is part of the Roundcube Webmail client | | Copyright (C) 2008-2012, The Roundcube Dev Team | @@ -23,9 +23,10 @@ /** * View class to produce JSON responses * - * @package View + * @package Core + * @subpackage View */ -class rcube_output_json extends rcube_output +class rcmail_output_json extends rcmail_output { protected $texts = array(); protected $commands = array(); @@ -157,7 +158,7 @@ class rcube_output_json extends rcube_output { $location = $this->app->url($p); $this->remote_response(sprintf("window.setTimeout(function(){ %s.redirect('%s',true); }, %d);", - rcmail::JS_OBJECT_NAME, $location, $delay)); + self::JS_OBJECT_NAME, $location, $delay)); exit; } diff --git a/program/include/rcmail_string_replacer.php b/program/include/rcmail_string_replacer.php new file mode 100644 index 000000000..4fbc611c9 --- /dev/null +++ b/program/include/rcmail_string_replacer.php @@ -0,0 +1,54 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | program/include/rcmail_string_replacer.php | + | | + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2012, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Turn URLs and email addresses into clickable links | + | | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Helper class for turning URLs and email addresses in plaintext content + * into clickable links. + * + * @package Core + * @subpackage Utils + */ +class rcmail_string_replacer extends rcube_string_replacer +{ + /** + * Callback function used to build mailto: links around e-mail strings + * + * This also adds an onclick-handler to open the Rouncube compose message screen on such links + * + * @param array Matches result from preg_replace_callback + * @return int Index of saved string value + * @see rcube_string_replacer::mailto_callback() + */ + public function mailto_callback($matches) + { + $href = $matches[1]; + $suffix = $this->parse_url_brackets($href); + + $i = $this->add(html::a(array( + 'href' => 'mailto:' . $href, + 'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('compose','".rcube::JQ($href)."',this)", + ), rcube::Q($href)) . $suffix); + + return $i >= 0 ? $this->get_replacement($i) : ''; + } + +}
\ No newline at end of file diff --git a/program/include/rcube.php b/program/include/rcube.php deleted file mode 100644 index 0e40b3c6b..000000000 --- a/program/include/rcube.php +++ /dev/null @@ -1,1251 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Framework base class providing core functions and holding | - | instances of all 'global' objects like db- and storage-connections | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Base class of the Roundcube Framework - * implemented as singleton - * - * @package Framework - * @subpackage Core - */ -class rcube -{ - const INIT_WITH_DB = 1; - const INIT_WITH_PLUGINS = 2; - - /** - * Singleton instace of rcube - * - * @var rcmail - */ - static protected $instance; - - /** - * Stores instance of rcube_config. - * - * @var rcube_config - */ - public $config; - - /** - * Instace of database class. - * - * @var rcube_db - */ - public $db; - - /** - * Instace of Memcache class. - * - * @var Memcache - */ - public $memcache; - - /** - * Instace of rcube_session class. - * - * @var rcube_session - */ - public $session; - - /** - * Instance of rcube_smtp class. - * - * @var rcube_smtp - */ - public $smtp; - - /** - * Instance of rcube_storage class. - * - * @var rcube_storage - */ - public $storage; - - /** - * Instance of rcube_output class. - * - * @var rcube_output - */ - public $output; - - /** - * Instance of rcube_plugin_api. - * - * @var rcube_plugin_api - */ - public $plugins; - - - /* private/protected vars */ - protected $texts; - protected $caches = array(); - protected $shutdown_functions = array(); - protected $expunge_cache = false; - - - /** - * This implements the 'singleton' design pattern - * - * @param integer Options to initialize with this instance. See rcube::INIT_WITH_* constants - * - * @return rcube The one and only instance - */ - static function get_instance($mode = 0) - { - if (!self::$instance) { - self::$instance = new rcube(); - self::$instance->init($mode); - } - - return self::$instance; - } - - - /** - * Private constructor - */ - protected function __construct() - { - // load configuration - $this->config = new rcube_config; - $this->plugins = new rcube_dummy_plugin_api; - - register_shutdown_function(array($this, 'shutdown')); - } - - - /** - * Initial startup function - */ - protected function init($mode = 0) - { - // initialize syslog - if ($this->config->get('log_driver') == 'syslog') { - $syslog_id = $this->config->get('syslog_id', 'roundcube'); - $syslog_facility = $this->config->get('syslog_facility', LOG_USER); - openlog($syslog_id, LOG_ODELAY, $syslog_facility); - } - - // connect to database - if ($mode & self::INIT_WITH_DB) { - $this->get_dbh(); - } - - // create plugin API and load plugins - if ($mode & self::INIT_WITH_PLUGINS) { - $this->plugins = rcube_plugin_api::get_instance(); - } - } - - - /** - * Get the current database connection - * - * @return rcube_db Database object - */ - public function get_dbh() - { - if (!$this->db) { - $config_all = $this->config->all(); - $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']); - $this->db->set_debug((bool)$config_all['sql_debug']); - } - - return $this->db; - } - - - /** - * Get global handle for memcache access - * - * @return object Memcache - */ - public function get_memcache() - { - if (!isset($this->memcache)) { - // no memcache support in PHP - if (!class_exists('Memcache')) { - $this->memcache = false; - return false; - } - - $this->memcache = new Memcache; - $this->mc_available = 0; - - // add all configured hosts to pool - $pconnect = $this->config->get('memcache_pconnect', true); - foreach ($this->config->get('memcache_hosts', array()) as $host) { - if (substr($host, 0, 7) != 'unix://') { - list($host, $port) = explode(':', $host); - if (!$port) $port = 11211; - } - else { - $port = 0; - } - - $this->mc_available += intval($this->memcache->addServer( - $host, $port, $pconnect, 1, 1, 15, false, array($this, 'memcache_failure'))); - } - - // test connection and failover (will result in $this->mc_available == 0 on complete failure) - $this->memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist - - if (!$this->mc_available) { - $this->memcache = false; - } - } - - return $this->memcache; - } - - - /** - * Callback for memcache failure - */ - public function memcache_failure($host, $port) - { - static $seen = array(); - - // only report once - if (!$seen["$host:$port"]++) { - $this->mc_available--; - self::raise_error(array( - 'code' => 604, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => "Memcache failure on host $host:$port"), - true, false); - } - } - - - /** - * Initialize and get cache object - * - * @param string $name Cache identifier - * @param string $type Cache type ('db', 'apc' or 'memcache') - * @param string $ttl Expiration time for cache items - * @param bool $packed Enables/disables data serialization - * - * @return rcube_cache Cache object - */ - public function get_cache($name, $type='db', $ttl=0, $packed=true) - { - if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) { - $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed); - } - - return $this->caches[$name]; - } - - - /** - * Create SMTP object and connect to server - * - * @param boolean True if connection should be established - */ - public function smtp_init($connect = false) - { - $this->smtp = new rcube_smtp(); - - if ($connect) { - $this->smtp->connect(); - } - } - - - /** - * Initialize and get storage object - * - * @return rcube_storage Storage object - */ - public function get_storage() - { - // already initialized - if (!is_object($this->storage)) { - $this->storage_init(); - } - - return $this->storage; - } - - - /** - * Initialize storage object - */ - public function storage_init() - { - // already initialized - if (is_object($this->storage)) { - return; - } - - $driver = $this->config->get('storage_driver', 'imap'); - $driver_class = "rcube_{$driver}"; - - if (!class_exists($driver_class)) { - self::raise_error(array( - 'code' => 700, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Storage driver class ($driver) not found!"), - true, true); - } - - // Initialize storage object - $this->storage = new $driver_class; - - // for backward compat. (deprecated, will be removed) - $this->imap = $this->storage; - - // enable caching of mail data - $storage_cache = $this->config->get("{$driver}_cache"); - $messages_cache = $this->config->get('messages_cache'); - // for backward compatybility - if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) { - $storage_cache = 'db'; - $messages_cache = true; - } - - if ($storage_cache) { - $this->storage->set_caching($storage_cache); - } - if ($messages_cache) { - $this->storage->set_messages_caching(true); - } - - // set pagesize from config - $pagesize = $this->config->get('mail_pagesize'); - if (!$pagesize) { - $pagesize = $this->config->get('pagesize', 50); - } - $this->storage->set_pagesize($pagesize); - - // set class options - $options = array( - 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), - 'auth_cid' => $this->config->get("{$driver}_auth_cid"), - 'auth_pw' => $this->config->get("{$driver}_auth_pw"), - 'debug' => (bool) $this->config->get("{$driver}_debug"), - 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), - 'timeout' => (int) $this->config->get("{$driver}_timeout"), - 'skip_deleted' => (bool) $this->config->get('skip_deleted'), - 'driver' => $driver, - ); - - if (!empty($_SESSION['storage_host'])) { - $options['host'] = $_SESSION['storage_host']; - $options['user'] = $_SESSION['username']; - $options['port'] = $_SESSION['storage_port']; - $options['ssl'] = $_SESSION['storage_ssl']; - $options['password'] = $this->decrypt($_SESSION['password']); - $_SESSION[$driver.'_host'] = $_SESSION['storage_host']; - } - - $options = $this->plugins->exec_hook("storage_init", $options); - - // for backward compat. (deprecated, to be removed) - $options = $this->plugins->exec_hook("imap_init", $options); - - $this->storage->set_options($options); - $this->set_storage_prop(); - } - - - /** - * Set storage parameters. - * This must be done AFTER connecting to the server! - */ - protected function set_storage_prop() - { - $storage = $this->get_storage(); - - $storage->set_charset($this->config->get('default_charset', RCMAIL_CHARSET)); - - if ($default_folders = $this->config->get('default_folders')) { - $storage->set_default_folders($default_folders); - } - if (isset($_SESSION['mbox'])) { - $storage->set_folder($_SESSION['mbox']); - } - if (isset($_SESSION['page'])) { - $storage->set_page($_SESSION['page']); - } - } - - - /** - * Create session object and start the session. - */ - public function session_init() - { - // session started (Installer?) - if (session_id()) { - return; - } - - $sess_name = $this->config->get('session_name'); - $sess_domain = $this->config->get('session_domain'); - $sess_path = $this->config->get('session_path'); - $lifetime = $this->config->get('session_lifetime', 0) * 60; - - // set session domain - if ($sess_domain) { - ini_set('session.cookie_domain', $sess_domain); - } - // set session path - if ($sess_path) { - ini_set('session.cookie_path', $sess_path); - } - // set session garbage collecting time according to session_lifetime - if ($lifetime) { - ini_set('session.gc_maxlifetime', $lifetime * 2); - } - - ini_set('session.cookie_secure', rcube_utils::https_check()); - ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid'); - ini_set('session.use_cookies', 1); - ini_set('session.use_only_cookies', 1); - ini_set('session.serialize_handler', 'php'); - ini_set('session.cookie_httponly', 1); - - // use database for storing session data - $this->session = new rcube_session($this->get_dbh(), $this->config); - - $this->session->register_gc_handler(array($this, 'temp_gc')); - $this->session->register_gc_handler(array($this, 'cache_gc')); - - // start PHP session (if not in CLI mode) - if ($_SERVER['REMOTE_ADDR']) { - session_start(); - } - } - - - /** - * Configure session object internals - */ - public function session_configure() - { - if (!$this->session) { - return; - } - - $lifetime = $this->config->get('session_lifetime', 0) * 60; - $keep_alive = $this->config->get('keep_alive'); - - // set keep-alive/check-recent interval - if ($keep_alive) { - // be sure that it's less than session lifetime - if ($lifetime) { - $keep_alive = min($keep_alive, $lifetime - 30); - } - $keep_alive = max(60, $keep_alive); - $this->session->set_keep_alive($keep_alive); - } - - $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); - $this->session->set_ip_check($this->config->get('ip_check')); - } - - - /** - * Garbage collector function for temp files. - * Remove temp files older than two days - */ - public function temp_gc() - { - $tmp = unslashify($this->config->get('temp_dir')); - $expire = time() - 172800; // expire in 48 hours - - if ($tmp && ($dir = opendir($tmp))) { - while (($fname = readdir($dir)) !== false) { - if ($fname{0} == '.') { - continue; - } - - if (filemtime($tmp.'/'.$fname) < $expire) { - @unlink($tmp.'/'.$fname); - } - } - - closedir($dir); - } - } - - - /** - * Garbage collector for cache entries. - * Set flag to expunge caches on shutdown - */ - public function cache_gc() - { - // because this gc function is called before storage is initialized, - // we just set a flag to expunge storage cache on shutdown. - $this->expunge_cache = true; - } - - - /** - * Get localized text in the desired language - * - * @param mixed $attrib Named parameters array or label name - * @param string $domain Label domain (plugin) name - * - * @return string Localized text - */ - public function gettext($attrib, $domain=null) - { - // load localization files if not done yet - if (empty($this->texts)) { - $this->load_language(); - } - - // extract attributes - if (is_string($attrib)) { - $attrib = array('name' => $attrib); - } - - $name = $attrib['name'] ? $attrib['name'] : ''; - - // attrib contain text values: use them from now - if (($setval = $attrib[strtolower($_SESSION['language'])]) || ($setval = $attrib['en_us'])) { - $this->texts[$name] = $setval; - } - - // check for text with domain - if ($domain && ($text = $this->texts[$domain.'.'.$name])) { - } - // text does not exist - else if (!($text = $this->texts[$name])) { - return "[$name]"; - } - - // replace vars in text - if (is_array($attrib['vars'])) { - foreach ($attrib['vars'] as $var_key => $var_value) { - $text = str_replace($var_key[0]!='$' ? '$'.$var_key : $var_key, $var_value, $text); - } - } - - // format output - if (($attrib['uppercase'] && strtolower($attrib['uppercase'] == 'first')) || $attrib['ucfirst']) { - return ucfirst($text); - } - else if ($attrib['uppercase']) { - return mb_strtoupper($text); - } - else if ($attrib['lowercase']) { - return mb_strtolower($text); - } - - return strtr($text, array('\n' => "\n")); - } - - - /** - * Check if the given text label exists - * - * @param string $name Label name - * @param string $domain Label domain (plugin) name or '*' for all domains - * @param string $ref_domain Sets domain name if label is found - * - * @return boolean True if text exists (either in the current language or in en_US) - */ - public function text_exists($name, $domain = null, &$ref_domain = null) - { - // load localization files if not done yet - if (empty($this->texts)) { - $this->load_language(); - } - - if (isset($this->texts[$name])) { - $ref_domain = ''; - return true; - } - - // any of loaded domains (plugins) - if ($domain == '*') { - foreach ($this->plugins->loaded_plugins() as $domain) { - if (isset($this->texts[$domain.'.'.$name])) { - $ref_domain = $domain; - return true; - } - } - } - // specified domain - else if ($domain) { - $ref_domain = $domain; - return isset($this->texts[$domain.'.'.$name]); - } - - return false; - } - - - /** - * Load a localization package - * - * @param string Language ID - * @param array Additional text labels/messages - */ - public function load_language($lang = null, $add = array()) - { - $lang = $this->language_prop(($lang ? $lang : $_SESSION['language'])); - - // load localized texts - if (empty($this->texts) || $lang != $_SESSION['language']) { - $this->texts = array(); - - // handle empty lines after closing PHP tag in localization files - ob_start(); - - // get english labels (these should be complete) - @include(INSTALL_PATH . 'program/localization/en_US/labels.inc'); - @include(INSTALL_PATH . 'program/localization/en_US/messages.inc'); - - if (is_array($labels)) - $this->texts = $labels; - if (is_array($messages)) - $this->texts = array_merge($this->texts, $messages); - - // include user language files - if ($lang != 'en' && $lang != 'en_US' && is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { - include_once(INSTALL_PATH . 'program/localization/' . $lang . '/labels.inc'); - include_once(INSTALL_PATH . 'program/localization/' . $lang . '/messages.inc'); - - if (is_array($labels)) - $this->texts = array_merge($this->texts, $labels); - if (is_array($messages)) - $this->texts = array_merge($this->texts, $messages); - } - - ob_end_clean(); - - $_SESSION['language'] = $lang; - } - - // append additional texts (from plugin) - if (is_array($add) && !empty($add)) { - $this->texts += $add; - } - } - - - /** - * Check the given string and return a valid language code - * - * @param string Language code - * - * @return string Valid language code - */ - protected function language_prop($lang) - { - static $rcube_languages, $rcube_language_aliases; - - // user HTTP_ACCEPT_LANGUAGE if no language is specified - if (empty($lang) || $lang == 'auto') { - $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); - $lang = str_replace('-', '_', $accept_langs[0]); - } - - if (empty($rcube_languages)) { - @include(INSTALL_PATH . 'program/localization/index.inc'); - } - - // check if we have an alias for that language - if (!isset($rcube_languages[$lang]) && isset($rcube_language_aliases[$lang])) { - $lang = $rcube_language_aliases[$lang]; - } - // try the first two chars - else if (!isset($rcube_languages[$lang])) { - $short = substr($lang, 0, 2); - - // check if we have an alias for the short language code - if (!isset($rcube_languages[$short]) && isset($rcube_language_aliases[$short])) { - $lang = $rcube_language_aliases[$short]; - } - // expand 'nn' to 'nn_NN' - else if (!isset($rcube_languages[$short])) { - $lang = $short.'_'.strtoupper($short); - } - } - - if (!isset($rcube_languages[$lang]) || !is_dir(INSTALL_PATH . 'program/localization/' . $lang)) { - $lang = 'en_US'; - } - - return $lang; - } - - - /** - * Read directory program/localization and return a list of available languages - * - * @return array List of available localizations - */ - public function list_languages() - { - static $sa_languages = array(); - - if (!sizeof($sa_languages)) { - @include(INSTALL_PATH . 'program/localization/index.inc'); - - if ($dh = @opendir(INSTALL_PATH . 'program/localization')) { - while (($name = readdir($dh)) !== false) { - if ($name[0] == '.' || !is_dir(INSTALL_PATH . 'program/localization/' . $name)) { - continue; - } - - if ($label = $rcube_languages[$name]) { - $sa_languages[$name] = $label; - } - } - closedir($dh); - } - } - - return $sa_languages; - } - - - /** - * Encrypt using 3DES - * - * @param string $clear clear text input - * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' - * @param boolean $base64 whether or not to base64_encode() the result before returning - * - * @return string encrypted text - */ - public function encrypt($clear, $key = 'des_key', $base64 = true) - { - if (!$clear) { - return ''; - } - - /*- - * Add a single canary byte to the end of the clear text, which - * will help find out how much of padding will need to be removed - * upon decryption; see http://php.net/mcrypt_generic#68082 - */ - $clear = pack("a*H2", $clear, "80"); - - if (function_exists('mcrypt_module_open') && - ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) - ) { - $iv = $this->create_iv(mcrypt_enc_get_iv_size($td)); - mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); - $cipher = $iv . mcrypt_generic($td, $clear); - mcrypt_generic_deinit($td); - mcrypt_module_close($td); - } - else { - @include_once 'des.inc'; - - if (function_exists('des')) { - $des_iv_size = 8; - $iv = $this->create_iv($des_iv_size); - $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv); - } - else { - self::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available" - ), true, true); - } - } - - return $base64 ? base64_encode($cipher) : $cipher; - } - - - /** - * Decrypt 3DES-encrypted string - * - * @param string $cipher encrypted text - * @param string $key encryption key to retrieve from the configuration, defaults to 'des_key' - * @param boolean $base64 whether or not input is base64-encoded - * - * @return string decrypted text - */ - public function decrypt($cipher, $key = 'des_key', $base64 = true) - { - if (!$cipher) { - return ''; - } - - $cipher = $base64 ? base64_decode($cipher) : $cipher; - - if (function_exists('mcrypt_module_open') && - ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) - ) { - $iv_size = mcrypt_enc_get_iv_size($td); - $iv = substr($cipher, 0, $iv_size); - - // session corruption? (#1485970) - if (strlen($iv) < $iv_size) { - return ''; - } - - $cipher = substr($cipher, $iv_size); - mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv); - $clear = mdecrypt_generic($td, $cipher); - mcrypt_generic_deinit($td); - mcrypt_module_close($td); - } - else { - @include_once 'des.inc'; - - if (function_exists('des')) { - $des_iv_size = 8; - $iv = substr($cipher, 0, $des_iv_size); - $cipher = substr($cipher, $des_iv_size); - $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv); - } - else { - self::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available" - ), true, true); - } - } - - /*- - * Trim PHP's padding and the canary byte; see note in - * rcube::encrypt() and http://php.net/mcrypt_generic#68082 - */ - $clear = substr(rtrim($clear, "\0"), 0, -1); - - return $clear; - } - - - /** - * Generates encryption initialization vector (IV) - * - * @param int Vector size - * - * @return string Vector string - */ - private function create_iv($size) - { - // mcrypt_create_iv() can be slow when system lacks entrophy - // we'll generate IV vector manually - $iv = ''; - for ($i = 0; $i < $size; $i++) { - $iv .= chr(mt_rand(0, 255)); - } - - return $iv; - } - - - /** - * Build a valid URL to this instance of Roundcube - * - * @param mixed Either a string with the action or url parameters as key-value pairs - * @return string Valid application URL - */ - public function url($p) - { - // STUB: should be overloaded by the application - return ''; - } - - - /** - * Function to be executed in script shutdown - * Registered with register_shutdown_function() - */ - public function shutdown() - { - foreach ($this->shutdown_functions as $function) { - call_user_func($function); - } - - if (is_object($this->smtp)) { - $this->smtp->disconnect(); - } - - foreach ($this->caches as $cache) { - if (is_object($cache)) { - $cache->close(); - } - } - - if (is_object($this->storage)) { - if ($this->expunge_cache) { - $this->storage->expunge_cache(); - } - $this->storage->close(); - } - } - - - /** - * Registers shutdown function to be executed on shutdown. - * The functions will be executed before destroying any - * objects like smtp, imap, session, etc. - * - * @param callback Function callback - */ - public function add_shutdown_function($function) - { - $this->shutdown_functions[] = $function; - } - - - /** - * Construct shell command, execute it and return output as string. - * Keywords {keyword} are replaced with arguments - * - * @param $cmd Format string with {keywords} to be replaced - * @param $values (zero, one or more arrays can be passed) - * - * @return output of command. shell errors not detectable - */ - public static function exec(/* $cmd, $values1 = array(), ... */) - { - $args = func_get_args(); - $cmd = array_shift($args); - $values = $replacements = array(); - - // merge values into one array - foreach ($args as $arg) { - $values += (array)$arg; - } - - preg_match_all('/({(-?)([a-z]\w*)})/', $cmd, $matches, PREG_SET_ORDER); - foreach ($matches as $tags) { - list(, $tag, $option, $key) = $tags; - $parts = array(); - - if ($option) { - foreach ((array)$values["-$key"] as $key => $value) { - if ($value === true || $value === false || $value === null) { - $parts[] = $value ? $key : ""; - } - else { - foreach ((array)$value as $val) { - $parts[] = "$key " . escapeshellarg($val); - } - } - } - } - else { - foreach ((array)$values[$key] as $value) { - $parts[] = escapeshellarg($value); - } - } - - $replacements[$tag] = join(" ", $parts); - } - - // use strtr behaviour of going through source string once - $cmd = strtr($cmd, $replacements); - - return (string)shell_exec($cmd); - } - - - /** - * Print or write debug messages - * - * @param mixed Debug message or data - */ - public static function console() - { - $args = func_get_args(); - - if (class_exists('rcube', false)) { - $rcube = self::get_instance(); - $plugin = $rcube->plugins->exec_hook('console', array('args' => $args)); - if ($plugin['abort']) { - return; - } - $args = $plugin['args']; - } - - $msg = array(); - foreach ($args as $arg) { - $msg[] = !is_string($arg) ? var_export($arg, true) : $arg; - } - - self::write_log('console', join(";\n", $msg)); - } - - - /** - * Append a line to a logfile in the logs directory. - * Date will be added automatically to the line. - * - * @param $name name of log file - * @param line Line to append - */ - public static function write_log($name, $line) - { - if (!is_string($line)) { - $line = var_export($line, true); - } - - $date_format = self::$instance ? self::$instance->config->get('log_date_format') : null; - $log_driver = self::$instance ? self::$instance->config->get('log_driver') : null; - - if (empty($date_format)) { - $date_format = 'd-M-Y H:i:s O'; - } - - $date = date($date_format); - - // trigger logging hook - if (is_object(self::$instance) && is_object(self::$instance->plugins)) { - $log = self::$instance->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line)); - $name = $log['name']; - $line = $log['line']; - $date = $log['date']; - if ($log['abort']) - return true; - } - - if ($log_driver == 'syslog') { - $prio = $name == 'errors' ? LOG_ERR : LOG_INFO; - syslog($prio, $line); - return true; - } - - // log_driver == 'file' is assumed here - - $line = sprintf("[%s]: %s\n", $date, $line); - $log_dir = self::$instance ? self::$instance->config->get('log_dir') : null; - - if (empty($log_dir)) { - $log_dir = INSTALL_PATH . 'logs'; - } - - // try to open specific log file for writing - $logfile = $log_dir.'/'.$name; - - if ($fp = @fopen($logfile, 'a')) { - fwrite($fp, $line); - fflush($fp); - fclose($fp); - return true; - } - - trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING); - return false; - } - - - /** - * Throw system error (and show error page). - * - * @param array Named parameters - * - code: Error code (required) - * - type: Error type [php|db|imap|javascript] (required) - * - message: Error message - * - file: File where error occured - * - line: Line where error occured - * @param boolean True to log the error - * @param boolean Terminate script execution - */ - public static function raise_error($arg = array(), $log = false, $terminate = false) - { - // handle PHP exceptions - if (is_object($arg) && is_a($arg, 'Exception')) { - $err = array( - 'type' => 'php', - 'code' => $arg->getCode(), - 'line' => $arg->getLine(), - 'file' => $arg->getFile(), - 'message' => $arg->getMessage(), - ); - $arg = $err; - } - - // installer - if (class_exists('rcube_install', false)) { - $rci = rcube_install::get_instance(); - $rci->raise_error($arg); - return; - } - - if (($log || $terminate) && $arg['type'] && $arg['message']) { - $arg['fatal'] = $terminate; - self::log_bug($arg); - } - - // display error page and terminate script - if ($terminate && is_object(self::$instance->output)) { - self::$instance->output->raise_error($arg['code'], $arg['message']); - } - } - - - /** - * Report error according to configured debug_level - * - * @param array Named parameters - * @see self::raise_error() - */ - public static function log_bug($arg_arr) - { - $program = strtoupper($arg_arr['type']); - $level = self::get_instance()->config->get('debug_level'); - - // disable errors for ajax requests, write to log instead (#1487831) - if (($level & 4) && !empty($_REQUEST['_remote'])) { - $level = ($level ^ 4) | 1; - } - - // write error to local log file - if (($level & 1) || !empty($arg_arr['fatal'])) { - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $post_query = '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']); - } - else { - $post_query = ''; - } - - $log_entry = sprintf("%s Error: %s%s (%s %s)", - $program, - $arg_arr['message'], - $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '', - $_SERVER['REQUEST_METHOD'], - $_SERVER['REQUEST_URI'] . $post_query); - - if (!self::write_log('errors', $log_entry)) { - // send error to PHPs error handler if write_log didn't succeed - trigger_error($arg_arr['message']); - } - } - - // report the bug to the global bug reporting system - if ($level & 2) { - // TODO: Send error via HTTP - } - - // show error if debug_mode is on - if ($level & 4) { - print "<b>$program Error"; - - if (!empty($arg_arr['file']) && !empty($arg_arr['line'])) { - print " in $arg_arr[file] ($arg_arr[line])"; - } - - print ':</b> '; - print nl2br($arg_arr['message']); - print '<br />'; - flush(); - } - } - - - /** - * Returns current time (with microseconds). - * - * @return float Current time in seconds since the Unix - */ - public static function timer() - { - return microtime(true); - } - - - /** - * Logs time difference according to provided timer - * - * @param float $timer Timer (self::timer() result) - * @param string $label Log line prefix - * @param string $dest Log file name - * - * @see self::timer() - */ - public static function print_timer($timer, $label = 'Timer', $dest = 'console') - { - static $print_count = 0; - - $print_count++; - $now = self::timer(); - $diff = $now - $timer; - - if (empty($label)) { - $label = 'Timer '.$print_count; - } - - self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff)); - } - - - /** - * Getter for logged user ID. - * - * @return mixed User identifier - */ - public function get_user_id() - { - if (is_object($this->user)) { - return $this->user->ID; - } - else if (isset($_SESSION['user_id'])) { - return $_SESSION['user_id']; - } - - return null; - } - - - /** - * Getter for logged user name. - * - * @return string User name - */ - public function get_user_name() - { - if (is_object($this->user)) { - return $this->user->get_username(); - } - - return null; - } -} - - -/** - * Lightweight plugin API class serving as a dummy if plugins are not enabled - * - * @package Core - */ -class rcube_dummy_plugin_api -{ - /** - * Triggers a plugin hook. - * @see rcube_plugin_api::exec_hook() - */ - public function exec_hook($hook, $args = array()) - { - return $args; - } -} diff --git a/program/include/rcube_addressbook.php b/program/include/rcube_addressbook.php deleted file mode 100644 index 892ae263a..000000000 --- a/program/include/rcube_addressbook.php +++ /dev/null @@ -1,532 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_addressbook.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Interface to the local address book database | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Abstract skeleton of an address book/repository - * - * @package Addressbook - */ -abstract class rcube_addressbook -{ - /** constants for error reporting **/ - const ERROR_READ_ONLY = 1; - const ERROR_NO_CONNECTION = 2; - const ERROR_VALIDATE = 3; - const ERROR_SAVING = 4; - const ERROR_SEARCH = 5; - - /** public properties (mandatory) */ - public $primary_key; - public $groups = false; - public $readonly = true; - public $searchonly = false; - public $undelete = false; - public $ready = false; - public $group_id = null; - public $list_page = 1; - public $page_size = 10; - public $sort_col = 'name'; - public $sort_order = 'ASC'; - public $coltypes = array('name' => array('limit'=>1), 'firstname' => array('limit'=>1), 'surname' => array('limit'=>1), 'email' => array('limit'=>1)); - - protected $error; - - /** - * Returns addressbook name (e.g. for addressbooks listing) - */ - abstract function get_name(); - - /** - * Save a search string for future listings - * - * @param mixed Search params to use in listing method, obtained by get_search_set() - */ - abstract function set_search_set($filter); - - /** - * Getter for saved search properties - * - * @return mixed Search properties used by this class - */ - abstract function get_search_set(); - - /** - * Reset saved results and search parameters - */ - abstract function reset(); - - /** - * Refresh saved search set after data has changed - * - * @return mixed New search set - */ - function refresh_search() - { - return $this->get_search_set(); - } - - /** - * List the current set of contact records - * - * @param array List of cols to show - * @param int Only return this number of records, use negative values for tail - * @return array Indexed list of contact records, each a hash array - */ - abstract function list_records($cols=null, $subset=0); - - /** - * Search records - * - * @param array List of fields to search in - * @param string Search value - * @param int Matching mode: - * 0 - partial (*abc*), - * 1 - strict (=), - * 2 - prefix (abc*) - * @param boolean True if results are requested, False if count only - * @param boolean True to skip the count query (select only) - * @param array List of fields that cannot be empty - * @return object rcube_result_set List of contact records and 'count' value - */ - abstract function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()); - - /** - * Count number of available contacts in database - * - * @return rcube_result_set Result set with values for 'count' and 'first' - */ - abstract function count(); - - /** - * Return the last result set - * - * @return rcube_result_set Current result set or NULL if nothing selected yet - */ - abstract function get_result(); - - /** - * Get a specific contact record - * - * @param mixed record identifier(s) - * @param boolean True to return record as associative array, otherwise a result set is returned - * - * @return mixed Result object with all record fields or False if not found - */ - abstract function get_record($id, $assoc=false); - - /** - * Returns the last error occured (e.g. when updating/inserting failed) - * - * @return array Hash array with the following fields: type, message - */ - function get_error() - { - return $this->error; - } - - /** - * Setter for errors for internal use - * - * @param int Error type (one of this class' error constants) - * @param string Error message (name of a text label) - */ - protected function set_error($type, $message) - { - $this->error = array('type' => $type, 'message' => $message); - } - - /** - * Close connection to source - * Called on script shutdown - */ - function close() { } - - /** - * Set internal list page - * - * @param number Page number to list - * @access public - */ - function set_page($page) - { - $this->list_page = (int)$page; - } - - /** - * Set internal page size - * - * @param number Number of messages to display on one page - * @access public - */ - function set_pagesize($size) - { - $this->page_size = (int)$size; - } - - /** - * Set internal sort settings - * - * @param string $sort_col Sort column - * @param string $sort_order Sort order - */ - function set_sort_order($sort_col, $sort_order = null) - { - if ($sort_col != null && ($this->coltypes[$sort_col] || in_array($sort_col, $this->coltypes))) { - $this->sort_col = $sort_col; - } - if ($sort_order != null) { - $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC'; - } - } - - /** - * Check the given data before saving. - * If input isn't valid, the message to display can be fetched using get_error() - * - * @param array Assoziative array with data to save - * @param boolean Attempt to fix/complete record automatically - * @return boolean True if input is valid, False if not. - */ - public function validate(&$save_data, $autofix = false) - { - $rcmail = rcmail::get_instance(); - - // 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 = $rcmail->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email))); - $this->set_error(self::ERROR_VALIDATE, $error); - return false; - } - } - } - - return true; - } - - - /** - * Create a new contact record - * - * @param array Assoziative array with save data - * Keys: Field name with optional section in the form FIELD:SECTION - * Values: Field value. Can be either a string or an array of strings for multiple values - * @param boolean True to check for duplicates first - * @return mixed The created record ID on success, False on error - */ - function insert($save_data, $check=false) - { - /* empty for read-only address books */ - } - - /** - * Create new contact records for every item in the record set - * - * @param object rcube_result_set Recordset to insert - * @param boolean True to check for duplicates first - * @return array List of created record IDs - */ - function insertMultiple($recset, $check=false) - { - $ids = array(); - if (is_object($recset) && is_a($recset, rcube_result_set)) { - while ($row = $recset->next()) { - if ($insert = $this->insert($row, $check)) - $ids[] = $insert; - } - } - return $ids; - } - - /** - * Update a specific contact record - * - * @param mixed Record identifier - * @param array Assoziative array with save data - * Keys: Field name with optional section in the form FIELD:SECTION - * Values: Field value. Can be either a string or an array of strings for multiple values - * @return boolean True on success, False on error - */ - function update($id, $save_cols) - { - /* empty for read-only address books */ - } - - /** - * Mark one or more contact records as deleted - * - * @param array Record identifiers - * @param bool Remove records irreversible (see self::undelete) - */ - function delete($ids, $force=true) - { - /* empty for read-only address books */ - } - - /** - * Unmark delete flag on contact record(s) - * - * @param array Record identifiers - */ - function undelete($ids) - { - /* empty for read-only address books */ - } - - /** - * Mark all records in database as deleted - */ - function delete_all() - { - /* empty for read-only address books */ - } - - /** - * Setter for the current group - * (empty, has to be re-implemented by extending class) - */ - function set_group($gid) { } - - /** - * List all active contact groups of this source - * - * @param string Optional search string to match group name - * @return array Indexed list of contact groups, each a hash array - */ - function list_groups($search = null) - { - /* empty for address books don't supporting groups */ - return array(); - } - - /** - * Get group properties such as name and email address(es) - * - * @param string Group identifier - * @return array Group properties as hash array - */ - function get_group($group_id) - { - /* empty for address books don't supporting groups */ - return null; - } - - /** - * Create a contact group with the given name - * - * @param string The group name - * @return mixed False on error, array with record props in success - */ - function create_group($name) - { - /* empty for address books don't supporting groups */ - return false; - } - - /** - * Delete the given group and all linked group members - * - * @param string Group identifier - * @return boolean True on success, false if no data was changed - */ - function delete_group($gid) - { - /* empty for address books don't supporting groups */ - return false; - } - - /** - * Rename a specific contact group - * - * @param string Group identifier - * @param string New name to set for this group - * @param string New group identifier (if changed, otherwise don't set) - * @return boolean New name on success, false if no data was changed - */ - function rename_group($gid, $newname, &$newid) - { - /* empty for address books don't supporting groups */ - return false; - } - - /** - * Add the given contact records the a certain group - * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added - */ - function add_to_group($group_id, $ids) - { - /* empty for address books don't supporting groups */ - return 0; - } - - /** - * Remove the given contact records from a certain group - * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members - */ - function remove_from_group($group_id, $ids) - { - /* empty for address books don't supporting groups */ - return 0; - } - - /** - * Get group assignments of a specific contact record - * - * @param mixed Record identifier - * - * @return array List of assigned groups as ID=>Name pairs - * @since 0.5-beta - */ - function get_record_groups($id) - { - /* empty for address books don't supporting groups */ - return array(); - } - - - /** - * Utility function to return all values of a certain data column - * either as flat list or grouped by subtype - * - * @param string Col name - * @param array Record data array as used for saving - * @param boolean True to return one array with all values, False for hash array with values grouped by type - * @return array List of column values - */ - function get_col_values($col, $data, $flat = false) - { - $out = array(); - foreach ((array)$data as $c => $values) { - if ($c === $col || strpos($c, $col.':') === 0) { - if ($flat) { - $out = array_merge($out, (array)$values); - } - else { - list($f, $type) = explode(':', $c); - $out[$type] = array_merge((array)$out[$type], (array)$values); - } - } - } - - // remove duplicates - if ($flat && !empty($out)) { - $out = array_unique($out); - } - - return $out; - } - - - /** - * Normalize the given string for fulltext search. - * Currently only optimized for Latin-1 characters; to be extended - * - * @param string Input string (UTF-8) - * @return string Normalized string - * @deprecated since 0.9-beta - */ - protected static function normalize_string($str) - { - return rcube_utils::normalize_string($str); - } - - /** - * Compose a valid display name from the given structured contact data - * - * @param array Hash array with contact data as key-value pairs - * @param bool Don't attempt to extract components from the email address - * - * @return string Display name - */ - public static function compose_display_name($contact, $full_email = false) - { - $contact = rcmail::get_instance()->plugins->exec_hook('contact_displayname', $contact); - $fn = $contact['name']; - - if (!$fn) // default display name composition according to vcard standard - $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])))); - - // use email address part for name - $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; - - if ($email && (empty($fn) || $fn == $email)) { - // return full email - if ($full_email) - return $email; - - list($emailname) = explode('@', $email); - if (preg_match('/(.*)[\.\-\_](.*)/', $emailname, $match)) - $fn = trim(ucfirst($match[1]).' '.ucfirst($match[2])); - else - $fn = ucfirst($emailname); - } - - return $fn; - } - - - /** - * Compose the name to display in the contacts list for the given contact record. - * This respects the settings parameter how to list conacts. - * - * @param array Hash array with contact data as key-value pairs - * @return string List name - */ - public static function compose_list_name($contact) - { - static $compose_mode; - - if (!isset($compose_mode)) // cache this - $compose_mode = rcmail::get_instance()->config->get('addressbook_name_listing', 0); - - if ($compose_mode == 3) - $fn = join(' ', array($contact['surname'] . ',', $contact['firstname'], $contact['middlename'])); - else if ($compose_mode == 2) - $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename'])); - else if ($compose_mode == 1) - $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname'])); - else - $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix'])); - - $fn = trim($fn, ', '); - - // fallback to display name - if (empty($fn) && $contact['name']) - $fn = $contact['name']; - - // fallback to email address - $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email']; - if (empty($fn) && $email) - return $email; - - return $fn; - } - -} - diff --git a/program/include/rcube_base_replacer.php b/program/include/rcube_base_replacer.php deleted file mode 100644 index 4ec367552..000000000 --- a/program/include/rcube_base_replacer.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_base_replacer.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | 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 Core - * @author Thomas Bruederli <roundcube@gmail.com> - */ -class rcube_base_replacer -{ - private $base_url; - - - public function __construct($base) - { - $this->base_url = $base; - } - - - public function callback($matches) - { - return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"'; - } - - - public function replace($body) - { - return preg_replace_callback(array( - '/(src|background|href)=(["\']?)([^"\'\s]+)(\2|\s|>)/Ui', - '/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/Ui', - ), - array($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) - { - $host_url = $base_url; - $abs_path = $path; - - // 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; - } - - // 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[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)) { - foreach ($matches as $a_match) { - if (strrpos($base_url, '/')) { - $base_url = substr($base_url, 0, strrpos($base_url, '/')); - } - $path = substr($path, 3); - } - } - - $abs_path = $base_url.'/'.$path; - } - - return $abs_path; - } -} diff --git a/program/include/rcube_browser.php b/program/include/rcube_browser.php deleted file mode 100644 index 7cfae709d..000000000 --- a/program/include/rcube_browser.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_browser.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2007-2009, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | 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 Core - */ -class rcube_browser -{ - function __construct() - { - $HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']); - - $this->ver = 0; - $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; - - $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false; - $this->ns4 = strpos($HTTP_USER_AGENT, 'mozilla/4') !== false && strpos($HTTP_USER_AGENT, 'msie') === false; - $this->ns = ($this->ns4 || strpos($HTTP_USER_AGENT, 'netscape') !== false); - $this->ie = !$this->opera && strpos($HTTP_USER_AGENT, 'compatible; msie') !== false; - $this->khtml = strpos($HTTP_USER_AGENT, 'khtml') !== false; - $this->mz = !$this->ie && !$this->khtml && strpos($HTTP_USER_AGENT, 'mozilla/5') !== false; - $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false; - $this->safari = !$this->chrome && ($this->khtml || strpos($HTTP_USER_AGENT, 'safari') !== false); - - if ($this->ns || $this->chrome) { - $test = preg_match('/(mozilla|chrome)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[2] : 0; - } - else if ($this->mz) { - $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[1] : 0; - } - else if ($this->ie || $this->opera) { - $test = preg_match('/(msie|opera) ([0-9.]+)/', $HTTP_USER_AGENT, $regs); - $this->ver = $test ? (float)$regs[2] : 0; - } - - if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs)) - $this->lang = $regs[1]; - else - $this->lang = 'en'; - - $this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7)); - $this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) || - ($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false; - $this->imgdata = !$this->ie; - } -} - diff --git a/program/include/rcube_cache.php b/program/include/rcube_cache.php deleted file mode 100644 index 4e60deaff..000000000 --- a/program/include/rcube_cache.php +++ /dev/null @@ -1,559 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_cache.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2011, The Roundcube Dev Team | - | Copyright (C) 2011, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Caching engine | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Interface class for accessing Roundcube cache - * - * @package Cache - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - * @version 1.1 - */ -class rcube_cache -{ - /** - * Instance of database handler - * - * @var rcube_db|Memcache|bool - */ - private $db; - private $type; - private $userid; - private $prefix; - private $ttl; - private $packed; - private $index; - private $cache = array(); - private $cache_changes = array(); - private $cache_sums = array(); - - - /** - * Object constructor. - * - * @param string $type Engine type ('db' or 'memcache' or 'apc') - * @param int $userid User identifier - * @param string $prefix Key name prefix - * @param string $ttl Expiration time of memcache/apc items - * @param bool $packed Enables/disabled data serialization. - * It's possible to disable data serialization if you're sure - * stored data will be always a safe string - */ - function __construct($type, $userid, $prefix='', $ttl=0, $packed=true) - { - $rcube = rcube::get_instance(); - $type = strtolower($type); - - if ($type == 'memcache') { - $this->type = 'memcache'; - $this->db = $rcube->get_memcache(); - } - else if ($type == 'apc') { - $this->type = 'apc'; - $this->db = function_exists('apc_exists'); // APC 3.1.4 required - } - else { - $this->type = 'db'; - $this->db = $rcube->get_dbh(); - } - - // convert ttl string to seconds - $ttl = get_offset_sec($ttl); - if ($ttl > 2592000) $ttl = 2592000; - - $this->userid = (int) $userid; - $this->ttl = $ttl; - $this->packed = $packed; - $this->prefix = $prefix; - } - - - /** - * Returns cached value. - * - * @param string $key Cache key name - * - * @return mixed Cached value - */ - function get($key) - { - if (!array_key_exists($key, $this->cache)) { - return $this->read_record($key); - } - - return $this->cache[$key]; - } - - - /** - * Sets (add/update) value in cache. - * - * @param string $key Cache key name - * @param mixed $data Cache data - */ - function set($key, $data) - { - $this->cache[$key] = $data; - $this->cache_changed = true; - $this->cache_changes[$key] = true; - } - - - /** - * Returns cached value without storing it in internal memory. - * - * @param string $key Cache key name - * - * @return mixed Cached value - */ - function read($key) - { - if (array_key_exists($key, $this->cache)) { - return $this->cache[$key]; - } - - return $this->read_record($key, true); - } - - - /** - * Sets (add/update) value in cache and immediately saves - * it in the backend, no internal memory will be used. - * - * @param string $key Cache key name - * @param mixed $data Cache data - * - * @param boolean True on success, False on failure - */ - function write($key, $data) - { - return $this->write_record($key, $this->packed ? serialize($data) : $data); - } - - - /** - * Clears the cache. - * - * @param string $key Cache key name or pattern - * @param boolean $prefix_mode Enable it to clear all keys starting - * with prefix specified in $key - */ - function remove($key=null, $prefix_mode=false) - { - // Remove all keys - if ($key === null) { - $this->cache = array(); - $this->cache_changed = false; - $this->cache_changes = array(); - $this->cache_sums = array(); - } - // Remove keys by name prefix - else if ($prefix_mode) { - foreach (array_keys($this->cache) as $k) { - if (strpos($k, $key) === 0) { - $this->cache[$k] = null; - $this->cache_changes[$k] = false; - unset($this->cache_sums[$k]); - } - } - } - // Remove one key by name - else { - $this->cache[$key] = null; - $this->cache_changes[$key] = false; - unset($this->cache_sums[$key]); - } - - // Remove record(s) from the backend - $this->remove_record($key, $prefix_mode); - } - - - /** - * Remove cache records older than ttl - */ - function expunge() - { - if ($this->type == 'db' && $this->db) { - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache'). - " WHERE user_id = ?". - " AND cache_key LIKE ?". - " AND " . $this->db->unixtimestamp('created')." < ?", - $this->userid, - $this->prefix.'.%', - time() - $this->ttl); - } - } - - - /** - * Writes the cache back to the DB. - */ - function close() - { - if (!$this->cache_changed) { - return; - } - - foreach ($this->cache as $key => $data) { - // The key has been used - if ($this->cache_changes[$key]) { - // Make sure we're not going to write unchanged data - // by comparing current md5 sum with the sum calculated on DB read - $data = $this->packed ? serialize($data) : $data; - - if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) { - $this->write_record($key, $data); - } - } - } - - $this->write_index(); - } - - - /** - * Reads cache entry. - * - * @param string $key Cache key name - * @param boolean $nostore Enable to skip in-memory store - * - * @return mixed Cached value - */ - private function read_record($key, $nostore=false) - { - if (!$this->db) { - return null; - } - - if ($this->type != 'db') { - if ($this->type == 'memcache') { - $data = $this->db->get($this->ckey($key)); - } - else if ($this->type == 'apc') { - $data = apc_fetch($this->ckey($key)); - } - - if ($data) { - $md5sum = md5($data); - $data = $this->packed ? unserialize($data) : $data; - - if ($nostore) { - return $data; - } - - $this->cache_sums[$key] = $md5sum; - $this->cache[$key] = $data; - } - else { - $this->cache[$key] = null; - } - } - else { - $sql_result = $this->db->limitquery( - "SELECT data, cache_key". - " FROM ".$this->db->table_name('cache'). - " WHERE user_id = ?". - " AND cache_key = ?". - // for better performance we allow more records for one key - // get the newer one - " ORDER BY created DESC", - 0, 1, $this->userid, $this->prefix.'.'.$key); - - if ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1); - $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null; - if ($sql_arr['data']) { - $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data']; - } - - if ($nostore) { - return $data; - } - - $this->cache[$key] = $data; - $this->cache_sums[$key] = $md5sum; - } - else { - $this->cache[$key] = null; - } - } - - return $this->cache[$key]; - } - - - /** - * Writes single cache record into DB. - * - * @param string $key Cache key name - * @param mxied $data Serialized cache data - * - * @param boolean True on success, False on failure - */ - private function write_record($key, $data) - { - if (!$this->db) { - return false; - } - - if ($this->type == 'memcache' || $this->type == 'apc') { - return $this->add_record($this->ckey($key), $data); - } - - $key_exists = array_key_exists($key, $this->cache_sums); - $key = $this->prefix . '.' . $key; - - // Remove NULL rows (here we don't need to check if the record exist) - if ($data == 'N;') { - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache'). - " WHERE user_id = ?". - " AND cache_key = ?", - $this->userid, $key); - - return true; - } - - // update existing cache record - if ($key_exists) { - $result = $this->db->query( - "UPDATE ".$this->db->table_name('cache'). - " SET created = ". $this->db->now().", data = ?". - " WHERE user_id = ?". - " AND cache_key = ?", - $data, $this->userid, $key); - } - // add new cache record - else { - // for better performance we allow more records for one key - // so, no need to check if record exist (see rcube_cache::read_record()) - $result = $this->db->query( - "INSERT INTO ".$this->db->table_name('cache'). - " (created, user_id, cache_key, data)". - " VALUES (".$this->db->now().", ?, ?, ?)", - $this->userid, $key, $data); - } - - return $this->db->affected_rows($result); - } - - - /** - * Deletes the cache record(s). - * - * @param string $key Cache key name or pattern - * @param boolean $prefix_mode Enable it to clear all keys starting - * with prefix specified in $key - * - */ - private function remove_record($key=null, $prefix_mode=false) - { - if (!$this->db) { - return; - } - - if ($this->type != 'db') { - $this->load_index(); - - // Remove all keys - if ($key === null) { - foreach ($this->index as $key) { - $this->delete_record($key, false); - } - $this->index = array(); - } - // Remove keys by name prefix - else if ($prefix_mode) { - foreach ($this->index as $k) { - if (strpos($k, $key) === 0) { - $this->delete_record($k); - } - } - } - // Remove one key by name - else { - $this->delete_record($key); - } - - return; - } - - // Remove all keys (in specified cache) - if ($key === null) { - $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%'); - } - // Remove keys by name prefix - else if ($prefix_mode) { - $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); - } - // Remove one key by name - else { - $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key); - } - - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache'). - " WHERE user_id = ?" . $where, - $this->userid); - } - - - /** - * Adds entry into memcache/apc DB. - * - * @param string $key Cache key name - * @param mxied $data Serialized cache data - * @param bollean $index Enables immediate index update - * - * @param boolean True on success, False on failure - */ - private function add_record($key, $data, $index=false) - { - if ($this->type == 'memcache') { - $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); - if (!$result) - $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); - } - else if ($this->type == 'apc') { - if (apc_exists($key)) - apc_delete($key); - $result = apc_store($key, $data, $this->ttl); - } - - // Update index - if ($index && $result) { - $this->load_index(); - - if (array_search($key, $this->index) === false) { - $this->index[] = $key; - $data = serialize($this->index); - $this->add_record($this->ikey(), $data); - } - } - - return $result; - } - - - /** - * Deletes entry from memcache/apc DB. - */ - private function delete_record($key, $index=true) - { - if ($this->type == 'memcache') { - // #1488592: use 2nd argument - $this->db->delete($this->ckey($key), 0); - } - else { - apc_delete($this->ckey($key)); - } - - if ($index) { - if (($idx = array_search($key, $this->index)) !== false) { - unset($this->index[$idx]); - } - } - } - - - /** - * Writes the index entry into memcache/apc DB. - */ - private function write_index() - { - if (!$this->db) { - return; - } - - if ($this->type == 'db') { - return; - } - - $this->load_index(); - - // Make sure index contains new keys - foreach ($this->cache as $key => $value) { - if ($value !== null) { - if (array_search($key, $this->index) === false) { - $this->index[] = $key; - } - } - } - - $data = serialize($this->index); - $this->add_record($this->ikey(), $data); - } - - - /** - * Gets the index entry from memcache/apc DB. - */ - private function load_index() - { - if (!$this->db) { - return; - } - - if ($this->index !== null) { - return; - } - - $index_key = $this->ikey(); - if ($this->type == 'memcache') { - $data = $this->db->get($index_key); - } - else if ($this->type == 'apc') { - $data = apc_fetch($index_key); - } - - $this->index = $data ? unserialize($data) : array(); - } - - - /** - * Creates per-user cache key name (for memcache and apc) - * - * @param string $key Cache key name - * - * @return string Cache key - */ - private function ckey($key) - { - return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key); - } - - - /** - * Creates per-user index cache key name (for memcache and apc) - * - * @return string Cache key - */ - private function ikey() - { - // This way each cache will have its own index - return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX'); - } -} diff --git a/program/include/rcube_charset.php b/program/include/rcube_charset.php deleted file mode 100644 index ff4c2bbce..000000000 --- a/program/include/rcube_charset.php +++ /dev/null @@ -1,763 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_charset.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org> | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Provide charset conversion functionality | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - -/** - * Character sets conversion functionality - * - * @package Core - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - * @author Edmund Grimley Evans <edmundo@rano.org> - */ -class rcube_charset -{ - // Aliases: some of them from HTML5 spec. - static public $aliases = array( - 'USASCII' => 'WINDOWS-1252', - 'ANSIX31101983' => 'WINDOWS-1252', - 'ANSIX341968' => 'WINDOWS-1252', - 'UNKNOWN8BIT' => 'ISO-8859-15', - 'UNKNOWN' => 'ISO-8859-15', - 'USERDEFINED' => 'ISO-8859-15', - 'KSC56011987' => 'EUC-KR', - 'GB2312' => 'GBK', - 'GB231280' => 'GBK', - 'UNICODE' => 'UTF-8', - 'UTF7IMAP' => 'UTF7-IMAP', - 'TIS620' => 'WINDOWS-874', - 'ISO88599' => 'WINDOWS-1254', - 'ISO885911' => 'WINDOWS-874', - 'MACROMAN' => 'MACINTOSH', - '77' => 'MAC', - '128' => 'SHIFT-JIS', - '129' => 'CP949', - '130' => 'CP1361', - '134' => 'GBK', - '136' => 'BIG5', - '161' => 'WINDOWS-1253', - '162' => 'WINDOWS-1254', - '163' => 'WINDOWS-1258', - '177' => 'WINDOWS-1255', - '178' => 'WINDOWS-1256', - '186' => 'WINDOWS-1257', - '204' => 'WINDOWS-1251', - '222' => 'WINDOWS-874', - '238' => 'WINDOWS-1250', - 'MS950' => 'CP950', - 'WINDOWS949' => 'UHC', - ); - - - /** - * Catch an error and throw an exception. - * - * @param int Level of the error - * @param string Error message - */ - public static function error_handler($errno, $errstr) - { - throw new ErrorException($errstr, 0, $errno); - } - - - /** - * Parse and validate charset name string (see #1485758). - * Sometimes charset string is malformed, there are also charset aliases - * but we need strict names for charset conversion (specially utf8 class) - * - * @param string $input Input charset name - * - * @return string The validated charset name - */ - public static function parse_charset($input) - { - static $charsets = array(); - $charset = strtoupper($input); - - if (isset($charsets[$input])) { - return $charsets[$input]; - } - - $charset = preg_replace(array( - '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO - '/\$.*$/', // e.g. _ISO-8859-JP$SIO - '/UNICODE-1-1-*/', // RFC1641/1642 - '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8) - ), '', $charset); - - if ($charset == 'BINARY') { - return $charsets[$input] = null; - } - - // allow A-Z and 0-9 only - $str = preg_replace('/[^A-Z0-9]/', '', $charset); - - if (isset(self::$aliases[$str])) { - $result = self::$aliases[$str]; - } - // UTF - else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) { - $result = 'UTF-' . $m[1] . $m[2]; - } - // ISO-8859 - else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) { - $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1); - // some clients sends windows-1252 text as latin1, - // it is safe to use windows-1252 for all latin1 - $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso; - } - // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE - else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) { - $result = 'WINDOWS-' . $m[2]; - } - // LATIN - else if (preg_match('/LATIN(.*)/', $str, $m)) { - $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10, - '7' => 13, '8' => 14, '9' => 15, '10' => 16, - 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8 - ); - - // some clients sends windows-1252 text as latin1, - // it is safe to use windows-1252 for all latin1 - if ($m[1] == 1) { - $result = 'WINDOWS-1252'; - } - // if iconv is not supported we need ISO labels, it's also safe for iconv - else if (!empty($aliases[$m[1]])) { - $result = 'ISO-8859-'.$aliases[$m[1]]; - } - // iconv requires convertion of e.g. LATIN-1 to LATIN1 - else { - $result = $str; - } - } - else { - $result = $charset; - } - - $charsets[$input] = $result; - - return $result; - } - - - /** - * Convert a string from one charset to another. - * Uses mbstring and iconv functions if possible - * - * @param string Input string - * @param string Suspected charset of the input string - * @param string Target charset to convert to; defaults to RCMAIL_CHARSET - * - * @return string Converted string - */ - public static function convert($str, $from, $to = null) - { - static $iconv_options = null; - static $mbstring_list = null; - static $mbstring_sch = null; - static $conv = null; - - $to = empty($to) ? RCMAIL_CHARSET : $to; - $from = self::parse_charset($from); - - // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) - // In that case we can just skip the conversion (use UTF-8) - if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) { - $from = 'UTF-8'; - } - - if ($from == $to || empty($str) || empty($from)) { - return $str; - } - - if ($iconv_options === null) { - if (function_exists('iconv')) { - // ignore characters not available in output charset - $iconv_options = '//IGNORE'; - if (iconv('', $iconv_options, '') === false) { - // iconv implementation does not support options - $iconv_options = ''; - } - } - } - - // convert charset using iconv module - if ($iconv_options !== null && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') { - // throw an exception if iconv reports an illegal character in input - // it means that input string has been truncated - set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE); - try { - $_iconv = iconv($from, $to . $iconv_options, $str); - } catch (ErrorException $e) { - $_iconv = false; - } - restore_error_handler(); - - if ($_iconv !== false) { - return $_iconv; - } - } - - if ($mbstring_list === null) { - if (extension_loaded('mbstring')) { - $mbstring_sch = mb_substitute_character(); - $mbstring_list = mb_list_encodings(); - $mbstring_list = array_map('strtoupper', $mbstring_list); - } - } - - // convert charset using mbstring module - if ($mbstring_list !== null) { - $aliases['WINDOWS-1257'] = 'ISO-8859-13'; - // it happens that mbstring supports ASCII but not US-ASCII - if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) { - $aliases['US-ASCII'] = 'ASCII'; - } - - $mb_from = $aliases[$from] ? $aliases[$from] : $from; - $mb_to = $aliases[$to] ? $aliases[$to] : $to; - - // return if encoding found, string matches encoding and convert succeeded - if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) { - if (mb_check_encoding($str, $mb_from)) { - // Do the same as //IGNORE with iconv - mb_substitute_character('none'); - $out = mb_convert_encoding($str, $mb_to, $mb_from); - mb_substitute_character($mbstring_sch); - - if ($out !== false) { - return $out; - } - } - } - } - - // convert charset using bundled classes/functions - if ($to == 'UTF-8') { - if ($from == 'UTF7-IMAP') { - if ($_str = self::utf7imap_to_utf8($str)) { - return $_str; - } - } - else if ($from == 'UTF-7') { - if ($_str = self::utf7_to_utf8($str)) { - return $_str; - } - } - else if ($from == 'ISO-8859-1' && function_exists('utf8_encode')) { - return utf8_encode($str); - } - else if (class_exists('utf8')) { - if (!$conv) { - $conv = new utf8($from); - } - else { - $conv->loadCharset($from); - } - - if ($_str = $conv->strToUtf8($str)) { - return $_str; - } - } - } - - // encode string for output - if ($from == 'UTF-8') { - // @TODO: we need a function for UTF-7 (RFC2152) conversion - if ($to == 'UTF7-IMAP' || $to == 'UTF-7') { - if ($_str = self::utf8_to_utf7imap($str)) { - return $_str; - } - } - else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) { - return utf8_decode($str); - } - else if (class_exists('utf8')) { - if (!$conv) { - $conv = new utf8($to); - } - else { - $conv->loadCharset($from); - } - - if ($_str = $conv->strToUtf8($str)) { - return $_str; - } - } - } - - // return original string - return $str; - } - - - /** - * Converts string from standard UTF-7 (RFC 2152) to UTF-8. - * - * @param string Input string (UTF-7) - * - * @return string Converted string (UTF-8) - */ - public static function utf7_to_utf8($str) - { - $Index_64 = array( - 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0, - 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0, - 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, - 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, - 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, - 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, - ); - - $u7len = strlen($str); - $str = strval($str); - $res = ''; - - for ($i=0; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - if ($u7 == '+') { - $i++; - $u7len--; - $ch = ''; - - for (; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - - if (!$Index_64[ord($u7)]) { - break; - } - - $ch .= $u7; - } - - if ($ch == '') { - if ($u7 == '-') { - $res .= '+'; - } - - continue; - } - - $res .= self::utf16_to_utf8(base64_decode($ch)); - } - else { - $res .= $u7; - } - } - - return $res; - } - - - /** - * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion) - * - * @param string Input string - * - * @return string The converted string - */ - public static function utf16_to_utf8($str) - { - $len = strlen($str); - $dec = ''; - - for ($i = 0; $i < $len; $i += 2) { - $c = ord($str[$i]) << 8 | ord($str[$i + 1]); - if ($c >= 0x0001 && $c <= 0x007F) { - $dec .= chr($c); - } - else if ($c > 0x07FF) { - $dec .= chr(0xE0 | (($c >> 12) & 0x0F)); - $dec .= chr(0x80 | (($c >> 6) & 0x3F)); - $dec .= chr(0x80 | (($c >> 0) & 0x3F)); - } - else { - $dec .= chr(0xC0 | (($c >> 6) & 0x1F)); - $dec .= chr(0x80 | (($c >> 0) & 0x3F)); - } - } - - return $dec; - } - - - /** - * Convert the data ($str) from RFC 2060's UTF-7 to UTF-8. - * If input data is invalid, return the original input string. - * RFC 2060 obviously intends the encoding to be unique (see - * point 5 in section 5.1.3), so we reject any non-canonical - * form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead - * of &AMAAwA-). - * - * Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com> - * - * @param string $str Input string (UTF7-IMAP) - * - * @return string Output string (UTF-8) - */ - public static function utf7imap_to_utf8($str) - { - $Index_64 = array( - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, 63,-1,-1,-1, - 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1,-1,-1,-1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, - 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, - -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, - 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 - ); - - $u7len = strlen($str); - $str = strval($str); - $p = ''; - $err = ''; - - for ($i=0; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - if ($u7 == '&') { - $i++; - $u7len--; - $u7 = $str[$i]; - - if ($u7len && $u7 == '-') { - $p .= '&'; - continue; - } - - $ch = 0; - $k = 10; - for (; $u7len > 0; $i++, $u7len--) { - $u7 = $str[$i]; - - if ((ord($u7) & 0x80) || ($b = $Index_64[ord($u7)]) == -1) { - break; - } - - if ($k > 0) { - $ch |= $b << $k; - $k -= 6; - } - else { - $ch |= $b >> (-$k); - if ($ch < 0x80) { - // Printable US-ASCII - if (0x20 <= $ch && $ch < 0x7f) { - return $err; - } - $p .= chr($ch); - } - else if ($ch < 0x800) { - $p .= chr(0xc0 | ($ch >> 6)); - $p .= chr(0x80 | ($ch & 0x3f)); - } - else { - $p .= chr(0xe0 | ($ch >> 12)); - $p .= chr(0x80 | (($ch >> 6) & 0x3f)); - $p .= chr(0x80 | ($ch & 0x3f)); - } - - $ch = ($b << (16 + $k)) & 0xffff; - $k += 10; - } - } - - // Non-zero or too many extra bits - if ($ch || $k < 6) { - return $err; - } - - // BASE64 not properly terminated - if (!$u7len || $u7 != '-') { - return $err; - } - - // Adjacent BASE64 sections - if ($u7len > 2 && $str[$i+1] == '&' && $str[$i+2] != '-') { - return $err; - } - } - // Not printable US-ASCII - else if (ord($u7) < 0x20 || ord($u7) >= 0x7f) { - return $err; - } - else { - $p .= $u7; - } - } - - return $p; - } - - - /** - * Convert the data ($str) from UTF-8 to RFC 2060's UTF-7. - * Unicode characters above U+FFFF are replaced by U+FFFE. - * If input data is invalid, return an empty string. - * - * Translated from C to PHP by Thomas Bruederli <roundcube@gmail.com> - * - * @param string $str Input string (UTF-8) - * - * @return string Output string (UTF7-IMAP) - */ - public static function utf8_to_utf7imap($str) - { - $B64Chars = array( - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', - 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', - 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', '+', ',' - ); - - $u8len = strlen($str); - $base64 = 0; - $i = 0; - $p = ''; - $err = ''; - - while ($u8len) { - $u8 = $str[$i]; - $c = ord($u8); - - if ($c < 0x80) { - $ch = $c; - $n = 0; - } - else if ($c < 0xc2) { - return $err; - } - else if ($c < 0xe0) { - $ch = $c & 0x1f; - $n = 1; - } - else if ($c < 0xf0) { - $ch = $c & 0x0f; - $n = 2; - } - else if ($c < 0xf8) { - $ch = $c & 0x07; - $n = 3; - } - else if ($c < 0xfc) { - $ch = $c & 0x03; - $n = 4; - } - else if ($c < 0xfe) { - $ch = $c & 0x01; - $n = 5; - } - else { - return $err; - } - - $i++; - $u8len--; - - if ($n > $u8len) { - return $err; - } - - for ($j=0; $j < $n; $j++) { - $o = ord($str[$i+$j]); - if (($o & 0xc0) != 0x80) { - return $err; - } - $ch = ($ch << 6) | ($o & 0x3f); - } - - if ($n > 1 && !($ch >> ($n * 5 + 1))) { - return $err; - } - - $i += $n; - $u8len -= $n; - - if ($ch < 0x20 || $ch >= 0x7f) { - if (!$base64) { - $p .= '&'; - $base64 = 1; - $b = 0; - $k = 10; - } - if ($ch & ~0xffff) { - $ch = 0xfffe; - } - - $p .= $B64Chars[($b | $ch >> $k)]; - $k -= 6; - for (; $k >= 0; $k -= 6) { - $p .= $B64Chars[(($ch >> $k) & 0x3f)]; - } - - $b = ($ch << (-$k)) & 0x3f; - $k += 16; - } - else { - if ($base64) { - if ($k > 10) { - $p .= $B64Chars[$b]; - } - $p .= '-'; - $base64 = 0; - } - - $p .= chr($ch); - if (chr($ch) == '&') { - $p .= '-'; - } - } - } - - if ($base64) { - if ($k > 10) { - $p .= $B64Chars[$b]; - } - $p .= '-'; - } - - return $p; - } - - - /** - * A method to guess character set of a string. - * - * @param string $string String. - * @param string $failover Default result for failover. - * - * @return string Charset name - */ - public static function detect($string, $failover='') - { - if (!function_exists('mb_detect_encoding')) { - return $failover; - } - - // FIXME: the order is important, because sometimes - // iso string is detected as euc-jp and etc. - $enc = array( - 'UTF-8', 'SJIS', 'BIG5', 'GB2312', - 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4', - 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', - 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', - 'WINDOWS-1252', 'WINDOWS-1251', 'EUC-JP', 'EUC-TW', 'KOI8-R', - 'ISO-2022-KR', 'ISO-2022-JP' - ); - - $result = mb_detect_encoding($string, join(',', $enc)); - - return $result ? $result : $failover; - } - - - /** - * Removes non-unicode characters from input. - * - * @param mixed $input String or array. - * - * @return mixed String or array - */ - public static function clean($input) - { - // handle input of type array - if (is_array($input)) { - foreach ($input as $idx => $val) { - $input[$idx] = self::clean($val); - } - return $input; - } - - if (!is_string($input) || $input == '') { - return $input; - } - - // iconv/mbstring are much faster (especially with long strings) - if (function_exists('mb_convert_encoding')) { - if (($res = mb_convert_encoding($input, 'UTF-8', 'UTF-8')) !== false) { - return $res; - } - } - - if (function_exists('iconv')) { - if (($res = @iconv('UTF-8', 'UTF-8//IGNORE', $input)) !== false) { - return $res; - } - } - - $seq = ''; - $out = ''; - $regexp = '/^('. -// '[\x00-\x7F]'. // UTF8-1 - '|[\xC2-\xDF][\x80-\xBF]'. // UTF8-2 - '|\xE0[\xA0-\xBF][\x80-\xBF]'. // UTF8-3 - '|[\xE1-\xEC][\x80-\xBF][\x80-\xBF]'. // UTF8-3 - '|\xED[\x80-\x9F][\x80-\xBF]'. // UTF8-3 - '|[\xEE-\xEF][\x80-\xBF][\x80-\xBF]'. // UTF8-3 - '|\xF0[\x90-\xBF][\x80-\xBF][\x80-\xBF]'. // UTF8-4 - '|[\xF1-\xF3][\x80-\xBF][\x80-\xBF][\x80-\xBF]'.// UTF8-4 - '|\xF4[\x80-\x8F][\x80-\xBF][\x80-\xBF]'. // UTF8-4 - ')$/'; - - for ($i = 0, $len = strlen($input); $i < $len; $i++) { - $chr = $input[$i]; - $ord = ord($chr); - - // 1-byte character - if ($ord <= 0x7F) { - if ($seq) { - $out .= preg_match($regexp, $seq) ? $seq : ''; - } - $seq = ''; - $out .= $chr; - // first (or second) byte of multibyte sequence - } - else if ($ord >= 0xC0) { - if (strlen($seq) > 1) { - $out .= preg_match($regexp, $seq) ? $seq : ''; - $seq = ''; - } - else if ($seq && ord($seq) < 0xC0) { - $seq = ''; - } - $seq .= $chr; - // next byte of multibyte sequence - } - else if ($seq) { - $seq .= $chr; - } - } - - if ($seq) { - $out .= preg_match($regexp, $seq) ? $seq : ''; - } - - return $out; - } - -} diff --git a/program/include/rcube_config.php b/program/include/rcube_config.php deleted file mode 100644 index b9fd95578..000000000 --- a/program/include/rcube_config.php +++ /dev/null @@ -1,418 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_config.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Class to read configuration settings | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - -/** - * Configuration class for Roundcube - * - * @package Core - */ -class rcube_config -{ - const DEFAULT_SKIN = 'larry'; - - private $prop = array(); - private $errors = array(); - private $userprefs = array(); - - /** - * Renamed options - * - * @var array - */ - private $legacy_props = array( - // new name => old name - 'default_folders' => 'default_imap_folders', - 'mail_pagesize' => 'pagesize', - 'addressbook_pagesize' => 'pagesize', - 'reply_mode' => 'top_posting', - ); - - - /** - * Object constructor - */ - public function __construct() - { - $this->load(); - - // Defaults, that we do not require you to configure, - // but contain information that is used in various - // locations in the code: - $this->set('contactlist_fields', array('name', 'firstname', 'surname', 'email')); - } - - - /** - * Load config from local config file - * - * @todo Remove global $CONFIG - */ - private function load() - { - // load main config file - if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/main.inc.php')) - $this->errors[] = 'main.inc.php was not found.'; - - // load database config - if (!$this->load_from_file(RCMAIL_CONFIG_DIR . '/db.inc.php')) - $this->errors[] = 'db.inc.php was not found.'; - - // load host-specific configuration - $this->load_host_config(); - - // set skin (with fallback to old 'skin_path' property) - if (empty($this->prop['skin'])) { - if (!empty($this->prop['skin_path'])) { - $this->prop['skin'] = str_replace('skins/', '', unslashify($this->prop['skin_path'])); - } - else { - $this->prop['skin'] = self::DEFAULT_SKIN; - } - } - - // larry is the new default skin :-) - if ($this->prop['skin'] == 'default') - $this->prop['skin'] = self::DEFAULT_SKIN; - - // fix paths - $this->prop['log_dir'] = $this->prop['log_dir'] ? realpath(unslashify($this->prop['log_dir'])) : INSTALL_PATH . 'logs'; - $this->prop['temp_dir'] = $this->prop['temp_dir'] ? realpath(unslashify($this->prop['temp_dir'])) : INSTALL_PATH . 'temp'; - - // fix default imap folders encoding - foreach (array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox') as $folder) - $this->prop[$folder] = rcube_charset::convert($this->prop[$folder], RCMAIL_CHARSET, 'UTF7-IMAP'); - - if (!empty($this->prop['default_folders'])) - foreach ($this->prop['default_folders'] as $n => $folder) - $this->prop['default_folders'][$n] = rcube_charset::convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP'); - - // set PHP error logging according to config - if ($this->prop['debug_level'] & 1) { - ini_set('log_errors', 1); - - if ($this->prop['log_driver'] == 'syslog') { - ini_set('error_log', 'syslog'); - } - else { - ini_set('error_log', $this->prop['log_dir'].'/errors'); - } - } - - // enable display_errors in 'show' level, but not for ajax requests - ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4))); - - // set timezone auto settings values - if ($this->prop['timezone'] == 'auto') { - $this->prop['_timezone_value'] = $this->client_timezone(); - } - else if (is_numeric($this->prop['timezone']) && ($tz = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0))) { - $this->prop['timezone'] = $tz; - } - else if (empty($this->prop['timezone'])) { - $this->prop['timezone'] = 'UTC'; - } - - // remove deprecated properties - unset($this->prop['dst_active']); - - // export config data - $GLOBALS['CONFIG'] = &$this->prop; - } - - /** - * Load a host-specific config file if configured - * This will merge the host specific configuration with the given one - */ - private function load_host_config() - { - $fname = null; - - if (is_array($this->prop['include_host_config'])) { - $fname = $this->prop['include_host_config'][$_SERVER['HTTP_HOST']]; - } - else if (!empty($this->prop['include_host_config'])) { - $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $_SERVER['HTTP_HOST']) . '.inc.php'; - } - - if ($fname) { - $this->load_from_file(RCMAIL_CONFIG_DIR . '/' . $fname); - } - } - - - /** - * Read configuration from a file - * and merge with the already stored config values - * - * @param string $fpath Full path to the config file to be loaded - * @return booelan True on success, false on failure - */ - public function load_from_file($fpath) - { - if (is_file($fpath) && is_readable($fpath)) { - // use output buffering, we don't need any output here - ob_start(); - include($fpath); - ob_end_clean(); - - if (is_array($rcmail_config)) { - $this->prop = array_merge($this->prop, $rcmail_config, $this->userprefs); - return true; - } - } - - return false; - } - - - /** - * Getter for a specific config parameter - * - * @param string $name Parameter name - * @param mixed $def Default value if not set - * @return mixed The requested config value - */ - public function get($name, $def = null) - { - if (isset($this->prop[$name])) { - $result = $this->prop[$name]; - } - else if (isset($this->legacy_props[$name])) { - return $this->get($this->legacy_props[$name], $def); - } - else { - $result = $def; - } - - $rcube = rcube::get_instance(); - - if ($name == 'timezone' && isset($this->prop['_timezone_value'])) - $result = $this->prop['_timezone_value']; - - $plugin = $rcube->plugins->exec_hook('config_get', array( - 'name' => $name, 'default' => $def, 'result' => $result)); - - return $plugin['result']; - } - - - /** - * Setter for a config parameter - * - * @param string $name Parameter name - * @param mixed $value Parameter value - */ - public function set($name, $value) - { - $this->prop[$name] = $value; - } - - - /** - * Override config options with the given values (eg. user prefs) - * - * @param array $prefs Hash array with config props to merge over - */ - public function merge($prefs) - { - $this->prop = array_merge($this->prop, $prefs, $this->userprefs); - } - - - /** - * Merge the given prefs over the current config - * and make sure that they survive further merging. - * - * @param array $prefs Hash array with user prefs - */ - public function set_user_prefs($prefs) - { - // Honor the dont_override setting for any existing user preferences - $dont_override = $this->get('dont_override'); - if (is_array($dont_override) && !empty($dont_override)) { - foreach ($dont_override as $key) { - unset($prefs[$key]); - } - } - - // convert user's timezone into the new format - if (is_numeric($prefs['timezone']) && ($tz = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0))) { - $prefs['timezone'] = $tz; - } - - // larry is the new default skin :-) - if ($prefs['skin'] == 'default') { - $prefs['skin'] = self::DEFAULT_SKIN; - } - - $this->userprefs = $prefs; - $this->prop = array_merge($this->prop, $prefs); - - // override timezone settings with client values - if ($this->prop['timezone'] == 'auto') { - $this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $this->client_timezone() : $this->prop['_timezone_value']; - } - else if (isset($this->prop['_timezone_value'])) - unset($this->prop['_timezone_value']); - } - - - /** - * Getter for all config options - * - * @return array Hash array containg all config properties - */ - public function all() - { - return $this->prop; - } - - /** - * Special getter for user's timezone offset including DST - * - * @return float Timezone offset (in hours) - * @deprecated - */ - public function get_timezone() - { - if ($tz = $this->get('timezone')) { - try { - $tz = new DateTimeZone($tz); - return $tz->getOffset(new DateTime('now')) / 3600; - } - catch (Exception $e) { - } - } - - return 0; - } - - /** - * Return requested DES crypto key. - * - * @param string $key Crypto key name - * @return string Crypto key - */ - public function get_crypto_key($key) - { - // Bomb out if the requested key does not exist - if (!array_key_exists($key, $this->prop)) { - rcube::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Request for unconfigured crypto key \"$key\"" - ), true, true); - } - - $key = $this->prop[$key]; - - // Bomb out if the configured key is not exactly 24 bytes long - if (strlen($key) != 24) { - rcube::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Configured crypto key '$key' is not exactly 24 bytes long" - ), true, true); - } - - return $key; - } - - - /** - * Try to autodetect operating system and find the correct line endings - * - * @return string The appropriate mail header delimiter - */ - public function header_delimiter() - { - // use the configured delimiter for headers - if (!empty($this->prop['mail_header_delimiter'])) { - $delim = $this->prop['mail_header_delimiter']; - if ($delim == "\n" || $delim == "\r\n") - return $delim; - else - rcube::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Invalid mail_header_delimiter setting" - ), true, false); - } - - $php_os = strtolower(substr(PHP_OS, 0, 3)); - - if ($php_os == 'win') - return "\r\n"; - - if ($php_os == 'mac') - return "\r\n"; - - return "\n"; - } - - - /** - * Return the mail domain configured for the given host - * - * @param string $host IMAP host - * @param boolean $encode If true, domain name will be converted to IDN ASCII - * @return string Resolved SMTP host - */ - public function mail_domain($host, $encode=true) - { - $domain = $host; - - if (is_array($this->prop['mail_domain'])) { - if (isset($this->prop['mail_domain'][$host])) - $domain = $this->prop['mail_domain'][$host]; - } - else if (!empty($this->prop['mail_domain'])) { - $domain = rcube_utils::parse_host($this->prop['mail_domain']); - } - - if ($encode) { - $domain = rcube_utils::idn_to_ascii($domain); - } - - return $domain; - } - - - /** - * Getter for error state - * - * @return mixed Error message on error, False if no errors - */ - public function get_error() - { - return empty($this->errors) ? false : join("\n", $this->errors); - } - - - /** - * Internal getter for client's (browser) timezone identifier - */ - private function client_timezone() - { - return isset($_SESSION['timezone']) && ($ctz = timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0)) ? $ctz : date_default_timezone_get(); - } - -} diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php deleted file mode 100644 index 534a65cb9..000000000 --- a/program/include/rcube_contacts.php +++ /dev/null @@ -1,1001 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_contacts.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Interface to the local address book database | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Model class for the local address book database - * - * @package Addressbook - */ -class rcube_contacts extends rcube_addressbook -{ - // protected for backward compat. with some plugins - protected $db_name = 'contacts'; - protected $db_groups = 'contactgroups'; - protected $db_groupmembers = 'contactgroupmembers'; - protected $vcard_fieldmap = array(); - - /** - * Store database connection. - * - * @var rcube_db - */ - private $db = null; - private $user_id = 0; - private $filter = null; - private $result = null; - private $cache; - private $table_cols = array('name', 'email', 'firstname', 'surname'); - private $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'nickname', - 'jobtitle', 'organization', 'department', 'maidenname', 'email', 'phone', - 'address', 'street', 'locality', 'zipcode', 'region', 'country', 'website', 'im', 'notes'); - - // public properties - public $primary_key = 'contact_id'; - public $name; - public $readonly = false; - public $groups = true; - public $undelete = true; - public $list_page = 1; - public $page_size = 10; - public $group_id = 0; - public $ready = false; - public $coltypes = array('name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname', - 'jobtitle', 'organization', 'department', 'assistant', 'manager', - 'gender', 'maidenname', 'spouse', 'email', 'phone', 'address', - 'birthday', 'anniversary', 'website', 'im', 'notes', 'photo'); - - const SEPARATOR = ','; - - - /** - * Object constructor - * - * @param object Instance of the rcube_db class - * @param integer User-ID - */ - function __construct($dbconn, $user) - { - $this->db = $dbconn; - $this->user_id = $user; - $this->ready = $this->db && !$this->db->is_error(); - } - - - /** - * Returns addressbook name - */ - function get_name() - { - return $this->name; - } - - - /** - * Save a search string for future listings - * - * @param string SQL params to use in listing method - */ - function set_search_set($filter) - { - $this->filter = $filter; - $this->cache = null; - } - - - /** - * Getter for saved search properties - * - * @return mixed Search properties used by this class - */ - function get_search_set() - { - return $this->filter; - } - - - /** - * Setter for the current group - * (empty, has to be re-implemented by extending class) - */ - function set_group($gid) - { - $this->group_id = $gid; - $this->cache = null; - } - - - /** - * Reset all saved results and search parameters - */ - function reset() - { - $this->result = null; - $this->filter = null; - $this->cache = null; - } - - - /** - * List all active contact groups of this source - * - * @param string Search string to match group name - * @return array Indexed list of contact groups, each a hash array - */ - function list_groups($search = null) - { - $results = array(); - - if (!$this->groups) - return $results; - - $sql_filter = $search ? " AND " . $this->db->ilike('name', '%'.$search.'%') : ''; - - $sql_result = $this->db->query( - "SELECT * FROM ".$this->db->table_name($this->db_groups). - " WHERE del<>1". - " AND user_id=?". - $sql_filter. - " ORDER BY name", - $this->user_id); - - while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $sql_arr['ID'] = $sql_arr['contactgroup_id']; - $results[] = $sql_arr; - } - - return $results; - } - - - /** - * Get group properties such as name and email address(es) - * - * @param string Group identifier - * @return array Group properties as hash array - */ - function get_group($group_id) - { - $sql_result = $this->db->query( - "SELECT * FROM ".$this->db->table_name($this->db_groups). - " WHERE del<>1". - " AND contactgroup_id=?". - " AND user_id=?", - $group_id, $this->user_id); - - if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $sql_arr['ID'] = $sql_arr['contactgroup_id']; - return $sql_arr; - } - - return null; - } - - /** - * List the current set of contact records - * - * @param array List of cols to show, Null means all - * @param int Only return this number of records, use negative values for tail - * @param boolean True to skip the count query (select only) - * @return array Indexed list of contact records, each a hash array - */ - function list_records($cols=null, $subset=0, $nocount=false) - { - if ($nocount || $this->list_page <= 1) { - // create dummy result, we don't need a count now - $this->result = new rcube_result_set(); - } else { - // count all records - $this->result = $this->count(); - } - - $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first; - $length = $subset != 0 ? abs($subset) : $this->page_size; - - if ($this->group_id) - $join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m". - " ON (m.contact_id = c.".$this->primary_key.")"; - - $order_col = (in_array($this->sort_col, $this->table_cols) ? $this->sort_col : 'name'); - $order_cols = array('c.'.$order_col); - if ($order_col == 'firstname') - $order_cols[] = 'c.surname'; - else if ($order_col == 'surname') - $order_cols[] = 'c.firstname'; - if ($order_col != 'name') - $order_cols[] = 'c.name'; - $order_cols[] = 'c.email'; - - $sql_result = $this->db->limitquery( - "SELECT * FROM ".$this->db->table_name($this->db_name)." AS c" . - $join . - " WHERE c.del<>1" . - " AND c.user_id=?" . - ($this->group_id ? " AND m.contactgroup_id=?" : ""). - ($this->filter ? " AND (".$this->filter.")" : "") . - " ORDER BY ". $this->db->concat($order_cols) . - " " . $this->sort_order, - $start_row, - $length, - $this->user_id, - $this->group_id); - - // determine whether we have to parse the vcard or if only db cols are requested - $read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols); - - while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $sql_arr['ID'] = $sql_arr[$this->primary_key]; - - if ($read_vcard) - $sql_arr = $this->convert_db_data($sql_arr); - else { - $sql_arr['email'] = explode(self::SEPARATOR, $sql_arr['email']); - $sql_arr['email'] = array_map('trim', $sql_arr['email']); - } - - $this->result->add($sql_arr); - } - - $cnt = count($this->result->records); - - // update counter - if ($nocount) - $this->result->count = $cnt; - else if ($this->list_page <= 1) { - if ($cnt < $this->page_size && $subset == 0) - $this->result->count = $cnt; - else if (isset($this->cache['count'])) - $this->result->count = $this->cache['count']; - else - $this->result->count = $this->_count(); - } - - return $this->result; - } - - - /** - * Search contacts - * - * @param mixed $fields The field name of array of field names to search in - * @param mixed $value Search value (or array of values when $fields is array) - * @param int $mode Matching mode: - * 0 - partial (*abc*), - * 1 - strict (=), - * 2 - prefix (abc*) - * @param boolean $select True if results are requested, False if count only - * @param boolean $nocount True to skip the count query (select only) - * @param array $required List of fields that cannot be empty - * - * @return object rcube_result_set Contact records and 'count' value - */ - function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()) - { - if (!is_array($fields)) - $fields = array($fields); - if (!is_array($required) && !empty($required)) - $required = array($required); - - $where = $and_where = array(); - $mode = intval($mode); - $WS = ' '; - $AS = self::SEPARATOR; - - foreach ($fields as $idx => $col) { - // direct ID search - if ($col == 'ID' || $col == $this->primary_key) { - $ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value; - $ids = $this->db->array2list($ids, 'integer'); - $where[] = 'c.' . $this->primary_key.' IN ('.$ids.')'; - continue; - } - // fulltext search in all fields - else if ($col == '*') { - $words = array(); - foreach (explode($WS, rcube_utils::normalize_string($value)) as $word) { - switch ($mode) { - case 1: // strict - $words[] = '(' . $this->db->ilike('words', $word . '%') - . ' OR ' . $this->db->ilike('words', '%' . $WS . $word . $WS . '%') - . ' OR ' . $this->db->ilike('words', '%' . $WS . $word) . ')'; - break; - case 2: // prefix - $words[] = '(' . $this->db->ilike('words', $word . '%') - . ' OR ' . $this->db->ilike('words', '%' . $WS . $word . '%') . ')'; - break; - default: // partial - $words[] = $this->db->ilike('words', '%' . $word . '%'); - } - } - $where[] = '(' . join(' AND ', $words) . ')'; - } - else { - $val = is_array($value) ? $value[$idx] : $value; - // table column - if (in_array($col, $this->table_cols)) { - switch ($mode) { - case 1: // strict - $where[] = '(' . $this->db->quoteIdentifier($col) . ' = ' . $this->db->quote($val) - . ' OR ' . $this->db->ilike($col, $val . $AS . '%') - . ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%') - . ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')'; - break; - case 2: // prefix - $where[] = '(' . $this->db->ilike($col, $val . '%') - . ' OR ' . $this->db->ilike($col, $AS . $val . '%') . ')'; - break; - default: // partial - $where[] = $this->db->ilike($col, '%' . $val . '%'); - } - } - // vCard field - else { - if (in_array($col, $this->fulltext_cols)) { - foreach (rcube_utils::normalize_string($val, true) as $word) { - switch ($mode) { - case 1: // strict - $words[] = '(' . $this->db->ilike('words', $word . $WS . '%') - . ' OR ' . $this->db->ilike('words', '%' . $AS . $word . $WS .'%') - . ' OR ' . $this->db->ilike('words', '%' . $AS . $word) . ')'; - break; - case 2: // prefix - $words[] = '(' . $this->db->ilike('words', $word . '%') - . ' OR ' . $this->db->ilike('words', $AS . $word . '%') . ')'; - break; - default: // partial - $words[] = $this->db->ilike('words', '%' . $word . '%'); - } - } - $where[] = '(' . join(' AND ', $words) . ')'; - } - if (is_array($value)) - $post_search[$col] = mb_strtolower($val); - } - } - } - - foreach (array_intersect($required, $this->table_cols) as $col) { - $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote(''); - } - - if (!empty($where)) { - // use AND operator for advanced searches - $where = join(is_array($value) ? ' AND ' : ' OR ', $where); - } - - if (!empty($and_where)) - $where = ($where ? "($where) AND " : '') . join(' AND ', $and_where); - - // Post-searching in vCard data fields - // we will search in all records and then build a where clause for their IDs - if (!empty($post_search)) { - $ids = array(0); - // build key name regexp - $regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/'; - // use initial WHERE clause, to limit records number if possible - if (!empty($where)) - $this->set_search_set($where); - - // count result pages - $cnt = $this->count(); - $pages = ceil($cnt / $this->page_size); - $scnt = count($post_search); - - // get (paged) result - for ($i=0; $i<$pages; $i++) { - $this->list_records(null, $i, true); - while ($row = $this->result->next()) { - $id = $row[$this->primary_key]; - $found = array(); - foreach (preg_grep($regexp, array_keys($row)) as $col) { - $pos = strpos($col, ':'); - $colname = $pos ? substr($col, 0, $pos) : $col; - $search = $post_search[$colname]; - foreach ((array)$row[$col] as $value) { - // composite field, e.g. address - foreach ((array)$value as $val) { - $val = mb_strtolower($val); - switch ($mode) { - case 1: - $got = ($val == $search); - break; - case 2: - $got = ($search == substr($val, 0, strlen($search))); - break; - default: - $got = (strpos($val, $search) !== false); - break; - } - - if ($got) { - $found[$colname] = true; - break 2; - } - } - } - } - // all fields match - if (count($found) >= $scnt) { - $ids[] = $id; - } - } - } - - // build WHERE clause - $ids = $this->db->array2list($ids, 'integer'); - $where = 'c.' . $this->primary_key.' IN ('.$ids.')'; - // reset counter - unset($this->cache['count']); - - // when we know we have an empty result - if ($ids == '0') { - $this->set_search_set($where); - return ($this->result = new rcube_result_set(0, 0)); - } - } - - if (!empty($where)) { - $this->set_search_set($where); - if ($select) - $this->list_records(null, 0, $nocount); - else - $this->result = $this->count(); - } - - return $this->result; - } - - - /** - * Count number of available contacts in database - * - * @return rcube_result_set Result object - */ - function count() - { - $count = isset($this->cache['count']) ? $this->cache['count'] : $this->_count(); - - return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); - } - - - /** - * Count number of available contacts in database - * - * @return int Contacts count - */ - private function _count() - { - if ($this->group_id) - $join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m". - " ON (m.contact_id=c.".$this->primary_key.")"; - - // count contacts for this user - $sql_result = $this->db->query( - "SELECT COUNT(c.contact_id) AS rows". - " FROM ".$this->db->table_name($this->db_name)." AS c". - $join. - " WHERE c.del<>1". - " AND c.user_id=?". - ($this->group_id ? " AND m.contactgroup_id=?" : ""). - ($this->filter ? " AND (".$this->filter.")" : ""), - $this->user_id, - $this->group_id - ); - - $sql_arr = $this->db->fetch_assoc($sql_result); - - $this->cache['count'] = (int) $sql_arr['rows']; - - return $this->cache['count']; - } - - - /** - * Return the last result set - * - * @return mixed Result array or NULL if nothing selected yet - */ - function get_result() - { - return $this->result; - } - - - /** - * Get a specific contact record - * - * @param mixed record identifier(s) - * @return mixed Result object with all record fields or False if not found - */ - function get_record($id, $assoc=false) - { - // return cached result - if ($this->result && ($first = $this->result->first()) && $first[$this->primary_key] == $id) - return $assoc ? $first : $this->result; - - $this->db->query( - "SELECT * FROM ".$this->db->table_name($this->db_name). - " WHERE contact_id=?". - " AND user_id=?". - " AND del<>1", - $id, - $this->user_id - ); - - if ($sql_arr = $this->db->fetch_assoc()) { - $record = $this->convert_db_data($sql_arr); - $this->result = new rcube_result_set(1); - $this->result->add($record); - } - - return $assoc && $record ? $record : $this->result; - } - - - /** - * Get group assignments of a specific contact record - * - * @param mixed Record identifier - * @return array List of assigned groups as ID=>Name pairs - */ - function get_record_groups($id) - { - $results = array(); - - if (!$this->groups) - return $results; - - $sql_result = $this->db->query( - "SELECT cgm.contactgroup_id, cg.name FROM " . $this->db->table_name($this->db_groupmembers) . " AS cgm" . - " LEFT JOIN " . $this->db->table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" . - " WHERE cgm.contact_id=?", - $id - ); - while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $results[$sql_arr['contactgroup_id']] = $sql_arr['name']; - } - - return $results; - } - - - /** - * Check the given data before saving. - * If input not valid, the message to display can be fetched using get_error() - * - * @param array Assoziative array with data to save - * @param boolean Try to fix/complete record automatically - * @return boolean True if input is valid, False if not. - */ - public function validate(&$save_data, $autofix = false) - { - // validate e-mail addresses - $valid = parent::validate($save_data, $autofix); - - // require at least one e-mail address (syntax check is already done) - if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) { - $this->set_error(self::ERROR_VALIDATE, 'noemailwarning'); - $valid = false; - } - - return $valid; - } - - - /** - * Create a new contact record - * - * @param array Associative array with save data - * @return integer|boolean The created record ID on success, False on error - */ - function insert($save_data, $check=false) - { - if (!is_array($save_data)) - return false; - - $insert_id = $existing = false; - - if ($check) { - foreach ($save_data as $col => $values) { - if (strpos($col, 'email') === 0) { - foreach ((array)$values as $email) { - if ($existing = $this->search('email', $email, false, false)) - break 2; - } - } - } - } - - $save_data = $this->convert_save_data($save_data); - $a_insert_cols = $a_insert_values = array(); - - foreach ($save_data as $col => $value) { - $a_insert_cols[] = $this->db->quoteIdentifier($col); - $a_insert_values[] = $this->db->quote($value); - } - - if (!$existing->count && !empty($a_insert_cols)) { - $this->db->query( - "INSERT INTO ".$this->db->table_name($this->db_name). - " (user_id, changed, del, ".join(', ', $a_insert_cols).")". - " VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")" - ); - - $insert_id = $this->db->insert_id($this->db_name); - } - - // also add the newly created contact to the active group - if ($insert_id && $this->group_id) - $this->add_to_group($this->group_id, $insert_id); - - $this->cache = null; - - return $insert_id; - } - - - /** - * Update a specific contact record - * - * @param mixed Record identifier - * @param array Assoziative array with save data - * @return boolean True on success, False on error - */ - function update($id, $save_cols) - { - $updated = false; - $write_sql = array(); - $record = $this->get_record($id, true); - $save_cols = $this->convert_save_data($save_cols, $record); - - foreach ($save_cols as $col => $value) { - $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value)); - } - - if (!empty($write_sql)) { - $this->db->query( - "UPDATE ".$this->db->table_name($this->db_name). - " SET changed=".$this->db->now().", ".join(', ', $write_sql). - " WHERE contact_id=?". - " AND user_id=?". - " AND del<>1", - $id, - $this->user_id - ); - - $updated = $this->db->affected_rows(); - $this->result = null; // clear current result (from get_record()) - } - - return $updated; - } - - - private function convert_db_data($sql_arr) - { - $record = array(); - $record['ID'] = $sql_arr[$this->primary_key]; - - if ($sql_arr['vcard']) { - unset($sql_arr['email']); - $vcard = new rcube_vcard($sql_arr['vcard'], RCMAIL_CHARSET, false, $this->vcard_fieldmap); - $record += $vcard->get_assoc() + $sql_arr; - } - else { - $record += $sql_arr; - $record['email'] = explode(self::SEPARATOR, $record['email']); - $record['email'] = array_map('trim', $record['email']); - } - - return $record; - } - - - private function convert_save_data($save_data, $record = array()) - { - $out = array(); - $words = ''; - - // copy values into vcard object - $vcard = new rcube_vcard($record['vcard'] ? $record['vcard'] : $save_data['vcard'], RCMAIL_CHARSET, false, $this->vcard_fieldmap); - $vcard->reset(); - foreach ($save_data as $key => $values) { - list($field, $section) = explode(':', $key); - $fulltext = in_array($field, $this->fulltext_cols); - foreach ((array)$values as $value) { - if (isset($value)) - $vcard->set($field, $value, $section); - if ($fulltext && is_array($value)) - $words .= ' ' . rcube_utils::normalize_string(join(" ", $value)); - else if ($fulltext && strlen($value) >= 3) - $words .= ' ' . rcube_utils::normalize_string($value); - } - } - $out['vcard'] = $vcard->export(false); - - foreach ($this->table_cols as $col) { - $key = $col; - if (!isset($save_data[$key])) - $key .= ':home'; - if (isset($save_data[$key])) { - if (is_array($save_data[$key])) - $out[$col] = join(self::SEPARATOR, $save_data[$key]); - else - $out[$col] = $save_data[$key]; - } - } - - // save all e-mails in database column - $out['email'] = join(self::SEPARATOR, $vcard->email); - - // join words for fulltext search - $out['words'] = join(" ", array_unique(explode(" ", $words))); - - return $out; - } - - - /** - * Mark one or more contact records as deleted - * - * @param array Record identifiers - * @param boolean Remove record(s) irreversible (unsupported) - */ - function delete($ids, $force=true) - { - if (!is_array($ids)) - $ids = explode(self::SEPARATOR, $ids); - - $ids = $this->db->array2list($ids, 'integer'); - - // flag record as deleted (always) - $this->db->query( - "UPDATE ".$this->db->table_name($this->db_name). - " SET del=1, changed=".$this->db->now(). - " WHERE user_id=?". - " AND contact_id IN ($ids)", - $this->user_id - ); - - $this->cache = null; - - return $this->db->affected_rows(); - } - - - /** - * Undelete one or more contact records - * - * @param array Record identifiers - */ - function undelete($ids) - { - if (!is_array($ids)) - $ids = explode(self::SEPARATOR, $ids); - - $ids = $this->db->array2list($ids, 'integer'); - - // clear deleted flag - $this->db->query( - "UPDATE ".$this->db->table_name($this->db_name). - " SET del=0, changed=".$this->db->now(). - " WHERE user_id=?". - " AND contact_id IN ($ids)", - $this->user_id - ); - - $this->cache = null; - - return $this->db->affected_rows(); - } - - - /** - * Remove all records from the database - */ - function delete_all() - { - $this->cache = null; - - $this->db->query("UPDATE ".$this->db->table_name($this->db_name). - " SET del=1, changed=".$this->db->now(). - " WHERE user_id = ?", $this->user_id); - - return $this->db->affected_rows(); - } - - - /** - * Create a contact group with the given name - * - * @param string The group name - * @return mixed False on error, array with record props in success - */ - function create_group($name) - { - $result = false; - - // make sure we have a unique name - $name = $this->unique_groupname($name); - - $this->db->query( - "INSERT INTO ".$this->db->table_name($this->db_groups). - " (user_id, changed, name)". - " VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")" - ); - - if ($insert_id = $this->db->insert_id($this->db_groups)) - $result = array('id' => $insert_id, 'name' => $name); - - return $result; - } - - - /** - * Delete the given group (and all linked group members) - * - * @param string Group identifier - * @return boolean True on success, false if no data was changed - */ - function delete_group($gid) - { - // flag group record as deleted - $sql_result = $this->db->query( - "UPDATE ".$this->db->table_name($this->db_groups). - " SET del=1, changed=".$this->db->now(). - " WHERE contactgroup_id=?". - " AND user_id=?", - $gid, $this->user_id - ); - - $this->cache = null; - - return $this->db->affected_rows(); - } - - - /** - * Rename a specific contact group - * - * @param string Group identifier - * @param string New name to set for this group - * @return boolean New name on success, false if no data was changed - */ - function rename_group($gid, $newname, &$new_gid) - { - // make sure we have a unique name - $name = $this->unique_groupname($newname); - - $sql_result = $this->db->query( - "UPDATE ".$this->db->table_name($this->db_groups). - " SET name=?, changed=".$this->db->now(). - " WHERE contactgroup_id=?". - " AND user_id=?", - $name, $gid, $this->user_id - ); - - return $this->db->affected_rows() ? $name : false; - } - - - /** - * Add the given contact records the a certain group - * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added - */ - function add_to_group($group_id, $ids) - { - if (!is_array($ids)) - $ids = explode(self::SEPARATOR, $ids); - - $added = 0; - $exists = array(); - - // get existing assignments ... - $sql_result = $this->db->query( - "SELECT contact_id FROM ".$this->db->table_name($this->db_groupmembers). - " WHERE contactgroup_id=?". - " AND contact_id IN (".$this->db->array2list($ids, 'integer').")", - $group_id - ); - while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $exists[] = $sql_arr['contact_id']; - } - // ... and remove them from the list - $ids = array_diff($ids, $exists); - - foreach ($ids as $contact_id) { - $this->db->query( - "INSERT INTO ".$this->db->table_name($this->db_groupmembers). - " (contactgroup_id, contact_id, created)". - " VALUES (?, ?, ".$this->db->now().")", - $group_id, - $contact_id - ); - - if ($error = $this->db->is_error()) - $this->set_error(self::ERROR_SAVING, $error); - else - $added++; - } - - return $added; - } - - - /** - * Remove the given contact records from a certain group - * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members - */ - function remove_from_group($group_id, $ids) - { - if (!is_array($ids)) - $ids = explode(self::SEPARATOR, $ids); - - $ids = $this->db->array2list($ids, 'integer'); - - $sql_result = $this->db->query( - "DELETE FROM ".$this->db->table_name($this->db_groupmembers). - " WHERE contactgroup_id=?". - " AND contact_id IN ($ids)", - $group_id - ); - - return $this->db->affected_rows(); - } - - - /** - * Check for existing groups with the same name - * - * @param string Name to check - * @return string A group name which is unique for the current use - */ - private function unique_groupname($name) - { - $checkname = $name; - $num = 2; $hit = false; - - do { - $sql_result = $this->db->query( - "SELECT 1 FROM ".$this->db->table_name($this->db_groups). - " WHERE del<>1". - " AND user_id=?". - " AND name=?", - $this->user_id, - $checkname); - - // append number to make name unique - if ($hit = $this->db->fetch_array($sql_result)) { - $checkname = $name . ' ' . $num++; - } - } while ($hit); - - return $checkname; - } - -} diff --git a/program/include/rcube_content_filter.php b/program/include/rcube_content_filter.php deleted file mode 100644 index 36e61a271..000000000 --- a/program/include/rcube_content_filter.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_content_filter.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2011, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | 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 - */ -class rcube_content_filter extends php_user_filter -{ - private $buffer = ''; - private $cutoff = 2048; - - function onCreate() - { - $this->cutoff = rand(2048, 3027); - return true; - } - - 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; - } -} diff --git a/program/include/rcube_db.php b/program/include/rcube_db.php deleted file mode 100644 index b066101b3..000000000 --- a/program/include/rcube_db.php +++ /dev/null @@ -1,1010 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_db.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Database independent query interface - * - * This is a wrapper for the PHP PDO - * - * @package Database - * @version 1.0 - */ -class rcube_db -{ - public $db_provider; - - protected $db_dsnw; // DSN for write operations - protected $db_dsnr; // DSN for read operations - protected $db_connected = false; // Already connected ? - protected $db_mode; // Connection mode - protected $dbh; // Connection handle - - protected $db_error = false; - protected $db_error_msg = ''; - protected $conn_failure = false; - protected $a_query_results = array('dummy'); - protected $last_res_id = 0; - protected $db_index = 0; - protected $tables; - protected $variables; - - protected $options = array( - // column/table quotes - 'identifier_start' => '"', - 'identifier_end' => '"', - ); - - - /** - * Factory, returns driver-specific instance of the class - * - * @param string $db_dsnw DSN for read/write operations - * @param string $db_dsnr Optional DSN for read only operations - * @param bool $pconn Enables persistent connections - * - * @return rcube_db Object instance - */ - public static function factory($db_dsnw, $db_dsnr = '', $pconn = false) - { - $driver = strtolower(substr($db_dsnw, 0, strpos($db_dsnw, ':'))); - $driver_map = array( - 'sqlite2' => 'sqlite', - 'sybase' => 'mssql', - 'dblib' => 'mssql', - 'mysqli' => 'mysql', - ); - - $driver = isset($driver_map[$driver]) ? $driver_map[$driver] : $driver; - $class = "rcube_db_$driver"; - - if (!class_exists($class)) { - rcube::raise_error(array('code' => 600, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => "Configuration error. Unsupported database driver: $driver"), - true, true); - } - - return new $class($db_dsnw, $db_dsnr, $pconn); - } - - /** - * Object constructor - * - * @param string $db_dsnw DSN for read/write operations - * @param string $db_dsnr Optional DSN for read only operations - * @param bool $pconn Enables persistent connections - */ - public function __construct($db_dsnw, $db_dsnr = '', $pconn = false) - { - if (empty($db_dsnr)) { - $db_dsnr = $db_dsnw; - } - - $this->db_dsnw = $db_dsnw; - $this->db_dsnr = $db_dsnr; - $this->db_pconn = $pconn; - - $this->db_dsnw_array = self::parse_dsn($db_dsnw); - $this->db_dsnr_array = self::parse_dsn($db_dsnr); - - // Initialize driver class - $this->init(); - } - - /** - * Initialization of the object with driver specific code - */ - protected function init() - { - // To be used by driver classes - } - - /** - * Connect to specific database - * - * @param array $dsn DSN for DB connections - * - * @return PDO database handle - */ - protected function dsn_connect($dsn) - { - $this->db_error = false; - $this->db_error_msg = null; - - // Get database specific connection options - $dsn_string = $this->dsn_string($dsn); - $dsn_options = $this->dsn_options($dsn); - - if ($db_pconn) { - $dsn_options[PDO::ATTR_PERSISTENT] = true; - } - - // Connect - try { - // with this check we skip fatal error on PDO object creation - if (!class_exists('PDO', false)) { - throw new Exception('PDO extension not loaded. See http://php.net/manual/en/intro.pdo.php'); - } - - $this->conn_prepare($dsn); - - $dbh = new PDO($dsn_string, $dsn['username'], $dsn['password'], $dsn_options); - - // don't throw exceptions or warnings - $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); - } - catch (Exception $e) { - $this->db_error = true; - $this->db_error_msg = $e->getMessage(); - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); - - return null; - } - - $this->conn_configure($dsn, $dbh); - - return $dbh; - } - - /** - * Driver-specific preparation of database connection - * - * @param array $dsn DSN for DB connections - */ - protected function conn_prepare($dsn) - { - } - - /** - * Driver-specific configuration of database connection - * - * @param array $dsn DSN for DB connections - * @param PDO $dbh Connection handler - */ - protected function conn_configure($dsn, $dbh) - { - } - - /** - * Driver-specific database character set setting - * - * @param string $charset Character set name - */ - protected function set_charset($charset) - { - $this->query("SET NAMES 'utf8'"); - } - - /** - * Connect to appropriate database depending on the operation - * - * @param string $mode Connection mode (r|w) - */ - public function db_connect($mode) - { - // previous connection failed, don't attempt to connect again - if ($this->conn_failure) { - return; - } - - // no replication - if ($this->db_dsnw == $this->db_dsnr) { - $mode = 'w'; - } - - // Already connected - if ($this->db_connected) { - // connected to db with the same or "higher" mode - if ($this->db_mode == 'w' || $this->db_mode == $mode) { - return; - } - } - - $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array; - - $this->dbh = $this->dsn_connect($dsn); - $this->db_connected = is_object($this->dbh); - - // use write-master when read-only fails - if (!$this->db_connected && $mode == 'r') { - $mode = 'w'; - $this->dbh = $this->dsn_connect($this->db_dsnw_array); - $this->db_connected = is_object($this->dbh); - } - - if ($this->db_connected) { - $this->db_mode = $mode; - $this->set_charset('utf8'); - } - else { - $this->conn_failure = true; - } - } - - /** - * Activate/deactivate debug mode - * - * @param boolean $dbg True if SQL queries should be logged - */ - public function set_debug($dbg = true) - { - $this->options['debug_mode'] = $dbg; - } - - /** - * Writes debug information/query to 'sql' log file - * - * @param string $query SQL query - */ - protected function debug($query) - { - if ($this->options['debug_mode']) { - rcube::write_log('sql', '[' . (++$this->db_index) . '] ' . $query . ';'); - } - } - - /** - * Getter for error state - * - * @param int $res_id Optional query result identifier - * - * @return string Error message - */ - public function is_error($res_id = null) - { - if ($res_id !== null) { - return $this->_get_result($res_id) === false ? $this->db_error_msg : null; - } - - return $this->db_error ? $this->db_error_msg : null; - } - - /** - * Connection state checker - * - * @return boolean True if in connected state - */ - public function is_connected() - { - return !is_object($this->dbh) ? false : $this->db_connected; - } - - /** - * Is database replication configured? - * - * @return bool Returns true if dsnw != dsnr - */ - public function is_replicated() - { - return !empty($this->db_dsnr) && $this->db_dsnw != $this->db_dsnr; - } - - /** - * Get database runtime variables - * - * @param string $varname Variable name - * @param mixed $default Default value if variable is not set - * - * @return mixed Variable value or default - */ - public function get_variable($varname, $default = null) - { - // to be implemented by driver class - return $default; - } - - /** - * Execute a SQL query - * - * @param string SQL query to execute - * @param mixed Values to be inserted in query - * - * @return number Query handle identifier - */ - public function query() - { - $params = func_get_args(); - $query = array_shift($params); - - // Support one argument of type array, instead of n arguments - if (count($params) == 1 && is_array($params[0])) { - $params = $params[0]; - } - - return $this->_query($query, 0, 0, $params); - } - - /** - * Execute a SQL query with limits - * - * @param string SQL query to execute - * @param int Offset for LIMIT statement - * @param int Number of rows for LIMIT statement - * @param mixed Values to be inserted in query - * - * @return int Query handle identifier - */ - public function limitquery() - { - $params = func_get_args(); - $query = array_shift($params); - $offset = array_shift($params); - $numrows = array_shift($params); - - return $this->_query($query, $offset, $numrows, $params); - } - - /** - * Execute a SQL query with limits - * - * @param string $query SQL query to execute - * @param int $offset Offset for LIMIT statement - * @param int $numrows Number of rows for LIMIT statement - * @param array $params Values to be inserted in query - * - * @return int Query handle identifier - */ - protected function _query($query, $offset, $numrows, $params) - { - // Read or write ? - $mode = preg_match('/^(select|show)/i', ltrim($query)) ? 'r' : 'w'; - - $this->db_connect($mode); - - // check connection before proceeding - if (!$this->is_connected()) { - return null; - } - - if ($numrows || $offset) { - $query = $this->set_limit($query, $numrows, $offset); - } - - $params = (array) $params; - - // Because in Roundcube we mostly use queries that are - // executed only once, we will not use prepared queries - $pos = 0; - $idx = 0; - - while ($pos = strpos($query, '?', $pos)) { - if ($query[$pos+1] == '?') { // skip escaped ? - $pos += 2; - } - else { - $val = $this->quote($params[$idx++]); - unset($params[$idx-1]); - $query = substr_replace($query, $val, $pos, 1); - $pos += strlen($val); - } - } - - // replace escaped ? back to normal - $query = rtrim(strtr($query, array('??' => '?')), ';'); - - $this->debug($query); - - $query = $this->dbh->query($query); - - if ($query === false) { - $error = $this->dbh->errorInfo(); - $this->db_error = true; - $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); - } - - // add result, even if it's an error - return $this->_add_result($query); - } - - /** - * Get number of affected rows for the last query - * - * @param number $res_id Optional query handle identifier - * - * @return int Number of rows or false on failure - */ - public function affected_rows($res_id = null) - { - if ($result = $this->_get_result($res_id)) { - return $result->rowCount(); - } - - return 0; - } - - /** - * Get last inserted record ID - * - * @param string $table Table name (to find the incremented sequence) - * - * @return mixed ID or false on failure - */ - public function insert_id($table = '') - { - if (!$this->db_connected || $this->db_mode == 'r') { - return false; - } - - if ($table) { - // resolve table name - $table = $this->table_name($table); - } - - $id = $this->dbh->lastInsertId($table); - - return $id; - } - - /** - * Get an associative array for one row - * If no query handle is specified, the last query will be taken as reference - * - * @param int $res_id Optional query handle identifier - * - * @return mixed Array with col values or false on failure - */ - public function fetch_assoc($res_id = null) - { - $result = $this->_get_result($res_id); - return $this->_fetch_row($result, PDO::FETCH_ASSOC); - } - - /** - * Get an index array for one row - * If no query handle is specified, the last query will be taken as reference - * - * @param int $res_id Optional query handle identifier - * - * @return mixed Array with col values or false on failure - */ - public function fetch_array($res_id = null) - { - $result = $this->_get_result($res_id); - return $this->_fetch_row($result, PDO::FETCH_NUM); - } - - /** - * Get col values for a result row - * - * @param PDOStatement $result Result handle - * @param int $mode Fetch mode identifier - * - * @return mixed Array with col values or false on failure - */ - protected function _fetch_row($result, $mode) - { - if (!is_object($result) || !$this->is_connected()) { - return false; - } - - return $result->fetch($mode); - } - - /** - * Adds LIMIT,OFFSET clauses to the query - * - * @param string $query SQL query - * @param int $limit Number of rows - * @param int $offset Offset - * - * @return string SQL query - */ - protected function set_limit($query, $limit = 0, $offset = 0) - { - if ($limit) { - $query .= ' LIMIT ' . intval($limit); - } - - if ($offset) { - $query .= ' OFFSET ' . intval($offset); - } - - return $query; - } - - /** - * Returns list of tables in a database - * - * @return array List of all tables of the current database - */ - public function list_tables() - { - // get tables if not cached - if ($this->tables === null) { - $q = $this->query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES ORDER BY TABLE_NAME'); - - if ($res = $this->_get_result($q)) { - $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0); - } - else { - $this->tables = array(); - } - } - - return $this->tables; - } - - /** - * Returns list of columns in database table - * - * @param string $table Table name - * - * @return array List of table cols - */ - public function list_cols($table) - { - $q = $this->query('SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?', - array($table)); - - if ($res = $this->_get_result($q)) { - return $res->fetchAll(PDO::FETCH_COLUMN, 0); - } - - return array(); - } - - /** - * Formats input so it can be safely used in a query - * - * @param mixed $input Value to quote - * @param string $type Type of data - * - * @return string Quoted/converted string for use in query - */ - public function quote($input, $type = null) - { - // handle int directly for better performance - if ($type == 'integer' || $type == 'int') { - return intval($input); - } - - if (is_null($input)) { - return 'NULL'; - } - - // create DB handle if not available - if (!$this->dbh) { - $this->db_connect('r'); - } - - if ($this->dbh) { - $map = array( - 'bool' => PDO::PARAM_BOOL, - 'integer' => PDO::PARAM_INT, - ); - $type = isset($map[$type]) ? $map[$type] : PDO::PARAM_STR; - return strtr($this->dbh->quote($input, $type), array('?' => '??')); // escape ? - } - - return 'NULL'; - } - - /** - * Quotes a string so it can be safely used as a table or column name - * - * @param string $str Value to quote - * - * @return string Quoted string for use in query - * @deprecated Replaced by rcube_db::quote_identifier - * @see rcube_db::quote_identifier - */ - public function quoteIdentifier($str) - { - return $this->quote_identifier($str); - } - - /** - * Quotes a string so it can be safely used as a table or column name - * - * @param string $str Value to quote - * - * @return string Quoted string for use in query - */ - public function quote_identifier($str) - { - $start = $this->options['identifier_start']; - $end = $this->options['identifier_end']; - $name = array(); - - foreach (explode('.', $str) as $elem) { - $elem = str_replace(array($start, $end), '', $elem); - $name[] = $start . $elem . $end; - } - - return implode($name, '.'); - } - - /** - * Return SQL function for current time and date - * - * @return string SQL function to use in query - */ - public function now() - { - return "now()"; - } - - /** - * Return list of elements for use with SQL's IN clause - * - * @param array $arr Input array - * @param string $type Type of data - * - * @return string Comma-separated list of quoted values for use in query - */ - public function array2list($arr, $type = null) - { - if (!is_array($arr)) { - return $this->quote($arr, $type); - } - - foreach ($arr as $idx => $item) { - $arr[$idx] = $this->quote($item, $type); - } - - return implode(',', $arr); - } - - /** - * Return SQL statement to convert a field value into a unix timestamp - * - * This method is deprecated and should not be used anymore due to limitations - * of timestamp functions in Mysql (year 2038 problem) - * - * @param string $field Field name - * - * @return string SQL statement to use in query - * @deprecated - */ - public function unixtimestamp($field) - { - return "UNIX_TIMESTAMP($field)"; - } - - /** - * Return SQL statement to convert from a unix timestamp - * - * @param int $timestamp Unix timestamp - * - * @return string Date string in db-specific format - */ - public function fromunixtime($timestamp) - { - return date("'Y-m-d H:i:s'", $timestamp); - } - - /** - * Return SQL statement for case insensitive LIKE - * - * @param string $column Field name - * @param string $value Search value - * - * @return string SQL statement to use in query - */ - public function ilike($column, $value) - { - return $this->quote_identifier($column).' LIKE '.$this->quote($value); - } - - /** - * Abstract SQL statement for value concatenation - * - * @return string SQL statement to be used in query - */ - public function concat(/* col1, col2, ... */) - { - $args = func_get_args(); - if (is_array($args[0])) { - $args = $args[0]; - } - - return '(' . join(' || ', $args) . ')'; - } - - /** - * Encodes non-UTF-8 characters in string/array/object (recursive) - * - * @param mixed $input Data to fix - * - * @return mixed Properly UTF-8 encoded data - */ - public static function encode($input) - { - if (is_object($input)) { - foreach (get_object_vars($input) as $idx => $value) { - $input->$idx = self::encode($value); - } - return $input; - } - else if (is_array($input)) { - foreach ($input as $idx => $value) { - $input[$idx] = self::encode($value); - } - return $input; - } - - return utf8_encode($input); - } - - /** - * Decodes encoded UTF-8 string/object/array (recursive) - * - * @param mixed $input Input data - * - * @return mixed Decoded data - */ - public static function decode($input) - { - if (is_object($input)) { - foreach (get_object_vars($input) as $idx => $value) { - $input->$idx = self::decode($value); - } - return $input; - } - else if (is_array($input)) { - foreach ($input as $idx => $value) { - $input[$idx] = self::decode($value); - } - return $input; - } - - return utf8_decode($input); - } - - /** - * Adds a query result and returns a handle ID - * - * @param object $res Query handle - * - * @return int Handle ID - */ - protected function _add_result($res) - { - $this->last_res_id = sizeof($this->a_query_results); - $this->a_query_results[$this->last_res_id] = $res; - - return $this->last_res_id; - } - - /** - * Resolves a given handle ID and returns the according query handle - * If no ID is specified, the last resource handle will be returned - * - * @param int $res_id Handle ID - * - * @return mixed Resource handle or false on failure - */ - protected function _get_result($res_id = null) - { - if ($res_id == null) { - $res_id = $this->last_res_id; - } - - if (!empty($this->a_query_results[$res_id])) { - return $this->a_query_results[$res_id]; - } - - return false; - } - - /** - * Return correct name for a specific database table - * - * @param string $table Table name - * - * @return string Translated table name - */ - public function table_name($table) - { - $rcube = rcube::get_instance(); - - // return table name if configured - $config_key = 'db_table_'.$table; - - if ($name = $rcube->config->get($config_key)) { - return $name; - } - - return $table; - } - - /** - * MDB2 DSN string parser - * - * @param string $sequence Secuence name - * - * @return array DSN parameters - */ - public static function parse_dsn($dsn) - { - if (empty($dsn)) { - return null; - } - - // Find phptype and dbsyntax - if (($pos = strpos($dsn, '://')) !== false) { - $str = substr($dsn, 0, $pos); - $dsn = substr($dsn, $pos + 3); - } - else { - $str = $dsn; - $dsn = null; - } - - // Get phptype and dbsyntax - // $str => phptype(dbsyntax) - if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) { - $parsed['phptype'] = $arr[1]; - $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2]; - } - else { - $parsed['phptype'] = $str; - $parsed['dbsyntax'] = $str; - } - - if (empty($dsn)) { - return $parsed; - } - - // Get (if found): username and password - // $dsn => username:password@protocol+hostspec/database - if (($at = strrpos($dsn,'@')) !== false) { - $str = substr($dsn, 0, $at); - $dsn = substr($dsn, $at + 1); - if (($pos = strpos($str, ':')) !== false) { - $parsed['username'] = rawurldecode(substr($str, 0, $pos)); - $parsed['password'] = rawurldecode(substr($str, $pos + 1)); - } - else { - $parsed['username'] = rawurldecode($str); - } - } - - // Find protocol and hostspec - - // $dsn => proto(proto_opts)/database - if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) { - $proto = $match[1]; - $proto_opts = $match[2] ? $match[2] : false; - $dsn = $match[3]; - } - // $dsn => protocol+hostspec/database (old format) - else { - if (strpos($dsn, '+') !== false) { - list($proto, $dsn) = explode('+', $dsn, 2); - } - if ( strpos($dsn, '//') === 0 - && strpos($dsn, '/', 2) !== false - && $parsed['phptype'] == 'oci8' - ) { - //oracle's "Easy Connect" syntax: - //"username/password@[//]host[:port][/service_name]" - //e.g. "scott/tiger@//mymachine:1521/oracle" - $proto_opts = $dsn; - $pos = strrpos($proto_opts, '/'); - $dsn = substr($proto_opts, $pos + 1); - $proto_opts = substr($proto_opts, 0, $pos); - } - else if (strpos($dsn, '/') !== false) { - list($proto_opts, $dsn) = explode('/', $dsn, 2); - } - else { - $proto_opts = $dsn; - $dsn = null; - } - } - - // process the different protocol options - $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp'; - $proto_opts = rawurldecode($proto_opts); - if (strpos($proto_opts, ':') !== false) { - list($proto_opts, $parsed['port']) = explode(':', $proto_opts); - } - if ($parsed['protocol'] == 'tcp') { - $parsed['hostspec'] = $proto_opts; - } - else if ($parsed['protocol'] == 'unix') { - $parsed['socket'] = $proto_opts; - } - - // Get dabase if any - // $dsn => database - if ($dsn) { - // /database - if (($pos = strpos($dsn, '?')) === false) { - $parsed['database'] = rawurldecode($dsn); - // /database?param1=value1¶m2=value2 - } - else { - $parsed['database'] = rawurldecode(substr($dsn, 0, $pos)); - $dsn = substr($dsn, $pos + 1); - if (strpos($dsn, '&') !== false) { - $opts = explode('&', $dsn); - } - else { // database?param1=value1 - $opts = array($dsn); - } - foreach ($opts as $opt) { - list($key, $value) = explode('=', $opt); - if (!array_key_exists($key, $parsed) || false === $parsed[$key]) { - // don't allow params overwrite - $parsed[$key] = rawurldecode($value); - } - } - } - } - - return $parsed; - } - - /** - * Returns PDO DSN string from DSN array - * - * @param array $dsn DSN parameters - * - * @return string DSN string - */ - protected function dsn_string($dsn) - { - $params = array(); - $result = $dsn['phptype'] . ':'; - - if ($dsn['hostspec']) { - $params[] = 'host=' . $dsn['hostspec']; - } - - if ($dsn['port']) { - $params[] = 'port=' . $dsn['port']; - } - - if ($dsn['database']) { - $params[] = 'dbname=' . $dsn['database']; - } - - if (!empty($params)) { - $result .= implode(';', $params); - } - - return $result; - } - - /** - * Returns driver-specific connection options - * - * @param array $dsn DSN parameters - * - * @return array Connection options - */ - protected function dsn_options($dsn) - { - $result = array(); - - return $result; - } -} diff --git a/program/include/rcube_db_mssql.php b/program/include/rcube_db_mssql.php deleted file mode 100644 index 119647d95..000000000 --- a/program/include/rcube_db_mssql.php +++ /dev/null @@ -1,157 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_db_mssql.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | for MS SQL Server database | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Database independent query interface - * - * This is a wrapper for the PHP PDO - * - * @package Database - * @version 1.0 - */ -class rcube_db_mssql extends rcube_db -{ - public $db_provider = 'mssql'; - - /** - * Driver initialization - */ - protected function init() - { - $this->options['identifier_start'] = '['; - $this->options['identifier_end'] = ']'; - } - - /** - * Character setting - */ - protected function set_charset($charset) - { - // UTF-8 is default - } - - /** - * Return SQL function for current time and date - * - * @return string SQL function to use in query - */ - public function now() - { - return "getdate()"; - } - - /** - * Return SQL statement to convert a field value into a unix timestamp - * - * This method is deprecated and should not be used anymore due to limitations - * of timestamp functions in Mysql (year 2038 problem) - * - * @param string $field Field name - * - * @return string SQL statement to use in query - * @deprecated - */ - public function unixtimestamp($field) - { - return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())"; - } - - /** - * Abstract SQL statement for value concatenation - * - * @return string SQL statement to be used in query - */ - public function concat(/* col1, col2, ... */) - { - $args = func_get_args(); - - if (is_array($args[0])) { - $args = $args[0]; - } - - return '(' . join('+', $args) . ')'; - } - - /** - * Adds TOP (LIMIT,OFFSET) clause to the query - * - * @param string $query SQL query - * @param int $limit Number of rows - * @param int $offset Offset - * - * @return string SQL query - */ - protected function set_limit($query, $limit = 0, $offset = 0) - { - $limit = intval($limit); - $offset = intval($offset); - - $orderby = stristr($query, 'ORDER BY'); - if ($orderby !== false) { - $sort = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc'; - $order = str_ireplace('ORDER BY', '', $orderby); - $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order)); - } - - $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($limit + $offset) . ' ', $query); - - $query = 'SELECT * FROM (SELECT TOP ' . $limit . ' * FROM (' . $query . ') AS inner_tbl'; - if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' '; - $query .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC'; - } - $query .= ') AS outer_tbl'; - if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' ' . $sort; - } - - return $query; - } - - /** - * Returns PDO DSN string from DSN array - */ - protected function dsn_string($dsn) - { - $params = array(); - $result = $dsn['phptype'] . ':'; - - if ($dsn['hostspec']) { - $host = $dsn['hostspec']; - if ($dsn['port']) { - $host .= ',' . $dsn['port']; - } - $params[] = 'host=' . $host; - } - - if ($dsn['database']) { - $params[] = 'dbname=' . $dsn['database']; - } - - if (!empty($params)) { - $result .= implode(';', $params); - } - - return $result; - } -} diff --git a/program/include/rcube_db_mysql.php b/program/include/rcube_db_mysql.php deleted file mode 100644 index 2cdcf3021..000000000 --- a/program/include/rcube_db_mysql.php +++ /dev/null @@ -1,156 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_db_mysql.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | for MySQL database | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Database independent query interface - * - * This is a wrapper for the PHP PDO - * - * @package Database - * @version 1.0 - */ -class rcube_db_mysql extends rcube_db -{ - public $db_provider = 'mysql'; - - /** - * Driver initialization/configuration - */ - protected function init() - { - // SQL identifiers quoting - $this->options['identifier_start'] = '`'; - $this->options['identifier_end'] = '`'; - } - - /** - * Abstract SQL statement for value concatenation - * - * @return string SQL statement to be used in query - */ - public function concat(/* col1, col2, ... */) - { - $args = func_get_args(); - - if (is_array($args[0])) { - $args = $args[0]; - } - - return 'CONCAT(' . join(', ', $args) . ')'; - } - - /** - * Returns PDO DSN string from DSN array - * - * @param array $dsn DSN parameters - * - * @return string Connection string - */ - protected function dsn_string($dsn) - { - $params = array(); - $result = 'mysql:'; - - if ($dsn['database']) { - $params[] = 'dbname=' . $dsn['database']; - } - - if ($dsn['hostspec']) { - $params[] = 'host=' . $dsn['hostspec']; - } - - if ($dsn['port']) { - $params[] = 'port=' . $dsn['port']; - } - - if ($dsn['socket']) { - $params[] = 'unix_socket=' . $dsn['socket']; - } - - $params[] = 'charset=utf8'; - - if (!empty($params)) { - $result .= implode(';', $params); - } - - return $result; - } - - /** - * Returns driver-specific connection options - * - * @param array $dsn DSN parameters - * - * @return array Connection options - */ - protected function dsn_options($dsn) - { - $result = array(); - - if (!empty($dsn['key'])) { - $result[PDO::MYSQL_ATTR_KEY] = $dsn['key']; - } - - if (!empty($dsn['cipher'])) { - $result[PDO::MYSQL_ATTR_CIPHER] = $dsn['cipher']; - } - - if (!empty($dsn['cert'])) { - $result[PDO::MYSQL_ATTR_SSL_CERT] = $dsn['cert']; - } - - if (!empty($dsn['capath'])) { - $result[PDO::MYSQL_ATTR_SSL_CAPATH] = $dsn['capath']; - } - - if (!empty($dsn['ca'])) { - $result[PDO::MYSQL_ATTR_SSL_CA] = $dsn['ca']; - } - - return $result; - } - - /** - * Get database runtime variables - * - * @param string $varname Variable name - * @param mixed $default Default value if variable is not set - * - * @return mixed Variable value or default - */ - public function get_variable($varname, $default = null) - { - if (!isset($this->variables)) { - $this->variables = array(); - - $result = $this->query('SHOW VARIABLES'); - - while ($sql_arr = $this->fetch_array($result)) { - $this->variables[$row[0]] = $row[1]; - } - } - - return isset($this->variables[$varname]) ? $this->variables[$varname] : $default; - } - -} diff --git a/program/include/rcube_db_pgsql.php b/program/include/rcube_db_pgsql.php deleted file mode 100644 index 0d0caadde..000000000 --- a/program/include/rcube_db_pgsql.php +++ /dev/null @@ -1,137 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_db_pgsql.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | for PostgreSQL database | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Database independent query interface - * - * This is a wrapper for the PHP PDO - * - * @package Database - * @version 1.0 - */ -class rcube_db_pgsql extends rcube_db -{ - public $db_provider = 'postgres'; - - /** - * Get last inserted record ID - * - * @param string $table Table name (to find the incremented sequence) - * - * @return mixed ID or false on failure - */ - public function insert_id($table = null) - { - if (!$this->db_connected || $this->db_mode == 'r') { - return false; - } - - if ($table) { - $table = $this->sequence_name($table); - } - - $id = $this->dbh->lastInsertId($table); - - return $id; - } - - /** - * Return correct name for a specific database sequence - * - * @param string $sequence Secuence name - * - * @return string Translated sequence name - */ - protected function sequence_name($sequence) - { - $rcube = rcube::get_instance(); - - // return sequence name if configured - $config_key = 'db_sequence_'.$sequence; - - if ($name = $rcube->config->get($config_key)) { - return $name; - } - - return $sequence; - } - - /** - * Return SQL statement to convert a field value into a unix timestamp - * - * This method is deprecated and should not be used anymore due to limitations - * of timestamp functions in Mysql (year 2038 problem) - * - * @param string $field Field name - * - * @return string SQL statement to use in query - * @deprecated - */ - public function unixtimestamp($field) - { - return "EXTRACT (EPOCH FROM $field)"; - } - - /** - * Return SQL statement for case insensitive LIKE - * - * @param string $column Field name - * @param string $value Search value - * - * @return string SQL statement to use in query - */ - public function ilike($column, $value) - { - return $this->quote_identifier($column) . ' ILIKE ' . $this->quote($value); - } - - /** - * Get database runtime variables - * - * @param string $varname Variable name - * @param mixed $default Default value if variable is not set - * - * @return mixed Variable value or default - */ - public function get_variable($varname, $default = null) - { - // There's a known case when max_allowed_packet is queried - // PostgreSQL doesn't have such limit, return immediately - if ($varname == 'max_allowed_packet') { - return $default; - } - - if (!isset($this->variables)) { - $this->variables = array(); - - $result = $this->query('SHOW ALL'); - - while ($row = $this->fetch_array($result)) { - $this->variables[$row[0]] = $row[1]; - } - } - - return isset($this->variables[$varname]) ? $this->variables[$varname] : $default; - } - -} diff --git a/program/include/rcube_db_sqlite.php b/program/include/rcube_db_sqlite.php deleted file mode 100644 index a7397674b..000000000 --- a/program/include/rcube_db_sqlite.php +++ /dev/null @@ -1,180 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_db_sqlite.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | for SQLite database | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Database independent query interface - * - * This is a wrapper for the PHP PDO - * - * @package Database - * @version 1.0 - */ -class rcube_db_sqlite extends rcube_db -{ - public $db_provider = 'sqlite'; - - /** - * Database character set - */ - protected function set_charset($charset) - { - } - - /** - * Prepare connection - */ - protected function conn_prepare($dsn) - { - // Create database file, required by PDO to exist on connection - if (!empty($dsn['database']) && !file_exists($dsn['database'])) { - $created = touch($dsn['database']); - - // File mode setting, for compat. with MDB2 - if (!empty($dsn['mode']) && $created) { - chmod($dsn['database'], octdec($dsn['mode'])); - } - } - } - - /** - * Configure connection, create database if not exists - */ - protected function conn_configure($dsn, $dbh) - { - // we emulate via callback some missing functions - $dbh->sqliteCreateFunction('unix_timestamp', array('rcube_db_sqlite', 'sqlite_unix_timestamp'), 1); - $dbh->sqliteCreateFunction('now', array('rcube_db_sqlite', 'sqlite_now'), 0); - - // Initialize database structure in file is empty - if (!empty($dsn['database']) && !filesize($dsn['database'])) { - $data = file_get_contents(INSTALL_PATH . 'SQL/sqlite.initial.sql'); - - if (strlen($data)) { - $this->debug('INITIALIZE DATABASE'); - - $q = $dbh->exec($data); - - if ($q === false) { - $error = $dbh->errorInfo(); - $this->db_error = true; - $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]); - - rcube::raise_error(array('code' => 500, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => $this->db_error_msg), true, false); - } - } - } - } - - /** - * Callback for sqlite: unix_timestamp() - */ - public static function sqlite_unix_timestamp($timestamp = '') - { - $timestamp = trim($timestamp); - if (!$timestamp) { - $ret = time(); - } - else if (!preg_match('/^[0-9]+$/s', $timestamp)) { - $ret = strtotime($timestamp); - } - else { - $ret = $timestamp; - } - - return $ret; - } - - /** - * Callback for sqlite: now() - */ - public static function sqlite_now() - { - return date("Y-m-d H:i:s"); - } - - /** - * Returns list of tables in database - * - * @return array List of all tables of the current database - */ - public function list_tables() - { - if ($this->tables === null) { - $q = $this->query('SELECT name FROM sqlite_master' - .' WHERE type = \'table\' ORDER BY name'); - - if ($res = $this->_get_result($q)) { - $this->tables = $res->fetchAll(PDO::FETCH_COLUMN, 0); - } - else { - $this->tables = array(); - } - } - - return $this->tables; - } - - /** - * Returns list of columns in database table - * - * @param string $table Table name - * - * @return array List of table cols - */ - public function list_cols($table) - { - $q = $this->query('SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', - array('table', $table)); - - $columns = array(); - - if ($sql = $this->fetch_array($q)) { - $sql = $sql[0]; - $start_pos = strpos($sql, '('); - $end_pos = strrpos($sql, ')'); - $sql = substr($sql, $start_pos+1, $end_pos-$start_pos-1); - $lines = explode(',', $sql); - - foreach ($lines as $line) { - $line = explode(' ', trim($line)); - - if ($line[0] && strpos($line[0], '--') !== 0) { - $column = $line[0]; - $columns[] = trim($column, '"'); - } - } - } - - return $columns; - } - - /** - * Build DSN string for PDO constructor - */ - protected function dsn_string($dsn) - { - return $dsn['phptype'] . ':' . $dsn['database']; - } -} diff --git a/program/include/rcube_db_sqlsrv.php b/program/include/rcube_db_sqlsrv.php deleted file mode 100644 index e58bf0704..000000000 --- a/program/include/rcube_db_sqlsrv.php +++ /dev/null @@ -1,158 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_db_sqlsrv.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Database wrapper class that implements PHP PDO functions | - | for MS SQL Server database | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Database independent query interface - * - * This is a wrapper for the PHP PDO - * - * @package Database - * @version 1.0 - */ -class rcube_db_sqlsrv extends rcube_db -{ - public $db_provider = 'mssql'; - - /** - * Driver initialization - */ - protected function init() - { - $this->options['identifier_start'] = '['; - $this->options['identifier_end'] = ']'; - } - - /** - * Database character set setting - */ - protected function set_charset($charset) - { - // UTF-8 is default - } - - /** - * Return SQL function for current time and date - * - * @return string SQL function to use in query - */ - public function now() - { - return "getdate()"; - } - - /** - * Return SQL statement to convert a field value into a unix timestamp - * - * This method is deprecated and should not be used anymore due to limitations - * of timestamp functions in Mysql (year 2038 problem) - * - * @param string $field Field name - * - * @return string SQL statement to use in query - * @deprecated - */ - public function unixtimestamp($field) - { - return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())"; - } - - /** - * Abstract SQL statement for value concatenation - * - * @return string SQL statement to be used in query - */ - public function concat(/* col1, col2, ... */) - { - $args = func_get_args(); - - if (is_array($args[0])) { - $args = $args[0]; - } - - return '(' . join('+', $args) . ')'; - } - - /** - * Adds TOP (LIMIT,OFFSET) clause to the query - * - * @param string $query SQL query - * @param int $limit Number of rows - * @param int $offset Offset - * - * @return string SQL query - */ - protected function set_limit($query, $limit = 0, $offset = 0) - { - $limit = intval($limit); - $offset = intval($offset); - - $orderby = stristr($query, 'ORDER BY'); - if ($orderby !== false) { - $sort = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc'; - $order = str_ireplace('ORDER BY', '', $orderby); - $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order)); - } - - $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($limit + $offset) . ' ', $query); - - $query = 'SELECT * FROM (SELECT TOP ' . $limit . ' * FROM (' . $query . ') AS inner_tbl'; - if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' '; - $query .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC'; - } - $query .= ') AS outer_tbl'; - if ($orderby !== false) { - $query .= ' ORDER BY ' . $order . ' ' . $sort; - } - - return $query; - } - - /** - * Returns PDO DSN string from DSN array - */ - protected function dsn_string($dsn) - { - $params = array(); - $result = 'sqlsrv:'; - - if ($dsn['hostspec']) { - $host = $dsn['hostspec']; - - if ($dsn['port']) { - $host .= ',' . $dsn['port']; - } - $params[] = 'Server=' . $host; - } - - if ($dsn['database']) { - $params[] = 'Database=' . $dsn['database']; - } - - if (!empty($params)) { - $result .= implode(';', $params); - } - - return $result; - } -} diff --git a/program/include/rcube_image.php b/program/include/rcube_image.php deleted file mode 100644 index 80e8bd4f3..000000000 --- a/program/include/rcube_image.php +++ /dev/null @@ -1,251 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_image.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Image resizer and converter | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - -class rcube_image -{ - private $image_file; - - const TYPE_GIF = 1; - const TYPE_JPG = 2; - const TYPE_PNG = 3; - const TYPE_TIF = 4; - - public static $extensions = array( - self::TYPE_GIF => 'gif', - self::TYPE_JPG => 'jpg', - self::TYPE_PNG => 'png', - self::TYPE_TIF => 'tif', - ); - - - function __construct($filename) - { - $this->image_file = $filename; - } - - /** - * Get image properties. - * - * @return mixed Hash array with image props like type, width, height - */ - public function props() - { - // use GD extension - if (function_exists('getimagesize') && ($imsize = @getimagesize($this->image_file))) { - $width = $imsize[0]; - $height = $imsize[1]; - $gd_type = $imsize['2']; - $type = image_type_to_extension($imsize['2'], false); - } - - // use ImageMagick - if (!$type && ($data = $this->identify())) { - list($type, $width, $height) = $data; - } - - if ($type) { - return array( - 'type' => $type, - 'gd_type' => $gd_type, - 'width' => $width, - 'height' => $height, - ); - } - } - - /** - * Resize image to a given size - * - * @param int $size Max width/height size - * @param string $filename Output filename - * - * @return bool True on success, False on failure - */ - public function resize($size, $filename = null) - { - $result = false; - $rcube = rcube::get_instance(); - $convert = $rcube->config->get('im_convert_path', false); - $props = $this->props(); - - if (!$filename) { - $filename = $this->image_file; - } - - // use Imagemagick - if ($convert) { - $p['out'] = $filename; - $p['in'] = $this->image_file; - $p['size'] = $size.'x'.$size; - $type = $props['type']; - - if (!$type && ($data = $this->identify())) { - $type = $data[0]; - } - - $type = strtr($type, array("jpeg" => "jpg", "tiff" => "tif", "ps" => "eps", "ept" => "eps")); - $p += array('type' => $type, 'types' => "bmp,eps,gif,jp2,jpg,png,svg,tif", 'quality' => 75); - $p['-opts'] = array('-resize' => $size.'>'); - - if (in_array($type, explode(',', $p['types']))) { // Valid type? - $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace RGB -quality {quality} {-opts} {in} {type}:{out}', $p); - } - - if ($result === '') { - return true; - } - } - - // use GD extension - $gd_types = array(IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG); - if ($props['gd_type'] && in_array($props['gd_type'], $gd_types)) { - if ($props['gd_type'] == IMAGETYPE_JPEG) { - $image = imagecreatefromjpeg($this->image_file); - } - elseif($props['gd_type'] == IMAGETYPE_GIF) { - $image = imagecreatefromgif($this->image_file); - } - elseif($props['gd_type'] == IMAGETYPE_PNG) { - $image = imagecreatefrompng($this->image_file); - } - - $scale = $size / max($props['width'], $props['height']); - $width = $props['width'] * $scale; - $height = $props['height'] * $scale; - - $new_image = imagecreatetruecolor($width, $height); - - // Fix transparency of gif/png image - if ($props['gd_type'] != IMAGETYPE_JPEG) { - imagealphablending($new_image, false); - imagesavealpha($new_image, true); - $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127); - imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent); - } - - imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']); - $image = $new_image; - - 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) { - return true; - } - } - - // @TODO: print error to the log? - return false; - } - - /** - * Convert image to a given type - * - * @param int $type Destination file type (see class constants) - * @param string $filename Output filename (if empty, original file will be used - * and filename extension will be modified) - * - * @return bool True on success, False on failure - */ - public function convert($type, $filename = null) - { - $rcube = rcube::get_instance(); - $convert = $rcube->config->get('im_convert_path', false); - - if (!$filename) { - $filename = $this->image_file; - - // modify extension - if ($extension = self::$extensions[$type]) { - $filename = preg_replace('/\.[^.]+$/', '', $filename) . '.' . $extension; - } - } - - // use ImageMagick - if ($convert) { - $p['in'] = $this->image_file; - $p['out'] = $filename; - $p['type'] = self::$extensions[$type]; - - $result = rcube::exec($convert . ' 2>&1 -colorspace RGB -quality 75 {in} {type}:{out}', $p); - - if ($result === '') { - return true; - } - } - - // use GD extension (TIFF isn't supported) - $props = $this->props(); - $gd_types = array(IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG); - - if ($props['gd_type'] && in_array($props['gd_type'], $gd_types)) { - if ($props['gd_type'] == IMAGETYPE_JPEG) { - $image = imagecreatefromjpeg($this->image_file); - } - else if ($props['gd_type'] == IMAGETYPE_GIF) { - $image = imagecreatefromgif($this->image_file); - } - else if ($props['gd_type'] == IMAGETYPE_PNG) { - $image = imagecreatefrompng($this->image_file); - } - - 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); - } - } - - // @TODO: print error to the log? - return false; - } - - /** - * Identify command handler. - */ - private function identify() - { - $rcube = rcube::get_instance(); - - if ($cmd = $rcube->config->get('im_identify_path')) { - $args = array('in' => $this->image_file, 'format' => "%m %[fx:w] %[fx:h]"); - $id = rcube::exec($cmd. ' 2>/dev/null -format {format} {in}', $args); - - if ($id) { - return explode(' ', strtolower($id)); - } - } - } - -} diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php deleted file mode 100644 index a89fd164d..000000000 --- a/program/include/rcube_imap.php +++ /dev/null @@ -1,4167 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_imap.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | IMAP Storage Engine | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Interface class for accessing an IMAP server - * - * @package Mail - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - * @version 2.0 - */ -class rcube_imap extends rcube_storage -{ - /** - * Instance of rcube_imap_generic - * - * @var rcube_imap_generic - */ - public $conn; - - /** - * Instance of rcube_imap_cache - * - * @var rcube_imap_cache - */ - protected $mcache; - - /** - * Instance of rcube_cache - * - * @var rcube_cache - */ - protected $cache; - - /** - * Internal (in-memory) cache - * - * @var array - */ - protected $icache = array(); - - protected $list_page = 1; - protected $delimiter; - protected $namespace; - protected $sort_field = ''; - protected $sort_order = 'DESC'; - protected $struct_charset; - protected $uid_id_map = array(); - protected $msg_headers = array(); - protected $search_set; - protected $search_string = ''; - protected $search_charset = ''; - protected $search_sort_field = ''; - protected $search_threads = false; - protected $search_sorted = false; - protected $options = array('auth_method' => 'check'); - protected $caching = false; - protected $messages_caching = false; - protected $threading = false; - - - /** - * Object constructor. - */ - public function __construct() - { - $this->conn = new rcube_imap_generic(); - - // Set namespace and delimiter from session, - // so some methods would work before connection - if (isset($_SESSION['imap_namespace'])) { - $this->namespace = $_SESSION['imap_namespace']; - } - if (isset($_SESSION['imap_delimiter'])) { - $this->delimiter = $_SESSION['imap_delimiter']; - } - } - - - /** - * Magic getter for backward compat. - * - * @deprecated. - */ - public function __get($name) - { - if (isset($this->{$name})) { - return $this->{$name}; - } - } - - - /** - * Connect to an IMAP server - * - * @param string $host Host to connect - * @param string $user Username for IMAP account - * @param string $pass Password for IMAP account - * @param integer $port Port to connect to - * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection - * - * @return boolean TRUE on success, FALSE on failure - */ - public function connect($host, $user, $pass, $port=143, $use_ssl=null) - { - // check for OpenSSL support in PHP build - if ($use_ssl && extension_loaded('openssl')) { - $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl; - } - else if ($use_ssl) { - rcube::raise_error(array('code' => 403, 'type' => 'imap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "OpenSSL not available"), true, false); - $port = 143; - } - - $this->options['port'] = $port; - - if ($this->options['debug']) { - $this->set_debug(true); - - $this->options['ident'] = array( - 'name' => 'Roundcube Webmail', - 'version' => RCMAIL_VERSION, - 'php' => PHP_VERSION, - 'os' => PHP_OS, - 'command' => $_SERVER['REQUEST_URI'], - ); - } - - $attempt = 0; - do { - $data = rcube::get_instance()->plugins->exec_hook('imap_connect', - array_merge($this->options, array('host' => $host, 'user' => $user, - 'attempt' => ++$attempt))); - - if (!empty($data['pass'])) { - $pass = $data['pass']; - } - - $this->conn->connect($data['host'], $data['user'], $pass, $data); - } while(!$this->conn->connected() && $data['retry']); - - $config = array( - 'host' => $data['host'], - 'user' => $data['user'], - 'password' => $pass, - 'port' => $port, - 'ssl' => $use_ssl, - ); - - $this->options = array_merge($this->options, $config); - $this->connect_done = true; - - if ($this->conn->connected()) { - // get namespace and delimiter - $this->set_env(); - return true; - } - // write error log - else if ($this->conn->error) { - if ($pass && $user) { - $message = sprintf("Login failed for %s from %s. %s", - $user, rcube_utils::remote_ip(), $this->conn->error); - - rcube::raise_error(array('code' => 403, 'type' => 'imap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => $message), true, false); - } - } - - return false; - } - - - /** - * Close IMAP connection. - * Usually done on script shutdown - */ - public function close() - { - $this->conn->closeConnection(); - if ($this->mcache) { - $this->mcache->close(); - } - } - - - /** - * Check connection state, connect if not connected. - * - * @return bool Connection state. - */ - public function check_connection() - { - // Establish connection if it wasn't done yet - if (!$this->connect_done && !empty($this->options['user'])) { - return $this->connect( - $this->options['host'], - $this->options['user'], - $this->options['password'], - $this->options['port'], - $this->options['ssl'] - ); - } - - return $this->is_connected(); - } - - - /** - * Checks IMAP connection. - * - * @return boolean TRUE on success, FALSE on failure - */ - public function is_connected() - { - return $this->conn->connected(); - } - - - /** - * Returns code of last error - * - * @return int Error code - */ - public function get_error_code() - { - return $this->conn->errornum; - } - - - /** - * Returns text of last error - * - * @return string Error string - */ - public function get_error_str() - { - return $this->conn->error; - } - - - /** - * Returns code of last command response - * - * @return int Response code - */ - public function get_response_code() - { - switch ($this->conn->resultcode) { - case 'NOPERM': - return self::NOPERM; - case 'READ-ONLY': - return self::READONLY; - case 'TRYCREATE': - return self::TRYCREATE; - case 'INUSE': - return self::INUSE; - case 'OVERQUOTA': - return self::OVERQUOTA; - case 'ALREADYEXISTS': - return self::ALREADYEXISTS; - case 'NONEXISTENT': - return self::NONEXISTENT; - case 'CONTACTADMIN': - return self::CONTACTADMIN; - default: - return self::UNKNOWN; - } - } - - - /** - * Activate/deactivate debug mode - * - * @param boolean $dbg True if IMAP conversation should be logged - */ - public function set_debug($dbg = true) - { - $this->options['debug'] = $dbg; - $this->conn->setDebug($dbg, array($this, 'debug_handler')); - } - - - /** - * Set internal folder reference. - * All operations will be perfomed on this folder. - * - * @param string $folder Folder name - */ - public function set_folder($folder) - { - if ($this->folder == $folder) { - return; - } - - $this->folder = $folder; - - // clear messagecount cache for this folder - $this->clear_messagecount($folder); - } - - - /** - * Save a search result for future message listing methods - * - * @param array $set Search set, result from rcube_imap::get_search_set(): - * 0 - searching criteria, string - * 1 - search result, rcube_result_index|rcube_result_thread - * 2 - searching character set, string - * 3 - sorting field, string - * 4 - true if sorted, bool - */ - public function set_search_set($set) - { - $set = (array)$set; - - $this->search_string = $set[0]; - $this->search_set = $set[1]; - $this->search_charset = $set[2]; - $this->search_sort_field = $set[3]; - $this->search_sorted = $set[4]; - $this->search_threads = is_a($this->search_set, 'rcube_result_thread'); - } - - - /** - * Return the saved search set as hash array - * - * @return array Search set - */ - public function get_search_set() - { - if (empty($this->search_set)) { - return null; - } - - return array( - $this->search_string, - $this->search_set, - $this->search_charset, - $this->search_sort_field, - $this->search_sorted, - ); - } - - - /** - * Returns the IMAP server's capability. - * - * @param string $cap Capability name - * - * @return mixed Capability value or TRUE if supported, FALSE if not - */ - public function get_capability($cap) - { - $cap = strtoupper($cap); - $sess_key = "STORAGE_$cap"; - - if (!isset($_SESSION[$sess_key])) { - if (!$this->check_connection()) { - return false; - } - - $_SESSION[$sess_key] = $this->conn->getCapability($cap); - } - - return $_SESSION[$sess_key]; - } - - - /** - * Checks the PERMANENTFLAGS capability of the current folder - * and returns true if the given flag is supported by the IMAP server - * - * @param string $flag Permanentflag name - * - * @return boolean True if this flag is supported - */ - public function check_permflag($flag) - { - $flag = strtoupper($flag); - $imap_flag = $this->conn->flags[$flag]; - $perm_flags = $this->get_permflags($this->folder); - - return in_array_nocase($imap_flag, $perm_flags); - } - - - /** - * Returns PERMANENTFLAGS of the specified folder - * - * @param string $folder Folder name - * - * @return array Flags - */ - public function get_permflags($folder) - { - if (!strlen($folder)) { - return array(); - } -/* - Checking PERMANENTFLAGS is rather rare, so we disable caching of it - Re-think when we'll use it for more than only MDNSENT flag - - $cache_key = 'mailboxes.permanentflags.' . $folder; - $permflags = $this->get_cache($cache_key); - - if ($permflags !== null) { - return explode(' ', $permflags); - } -*/ - if (!$this->check_connection()) { - return array(); - } - - if ($this->conn->select($folder)) { - $permflags = $this->conn->data['PERMANENTFLAGS']; - } - else { - return array(); - } - - if (!is_array($permflags)) { - $permflags = array(); - } -/* - // Store permflags as string to limit cached object size - $this->update_cache($cache_key, implode(' ', $permflags)); -*/ - return $permflags; - } - - - /** - * Returns the delimiter that is used by the IMAP server for folder separation - * - * @return string Delimiter string - * @access public - */ - public function get_hierarchy_delimiter() - { - return $this->delimiter; - } - - - /** - * Get namespace - * - * @param string $name Namespace array index: personal, other, shared, prefix - * - * @return array Namespace data - */ - public function get_namespace($name = null) - { - $ns = $this->namespace; - - if ($name) { - return isset($ns[$name]) ? $ns[$name] : null; - } - - unset($ns['prefix']); - return $ns; - } - - - /** - * Sets delimiter and namespaces - */ - protected function set_env() - { - if ($this->delimiter !== null && $this->namespace !== null) { - return; - } - - $config = rcube::get_instance()->config; - $imap_personal = $config->get('imap_ns_personal'); - $imap_other = $config->get('imap_ns_other'); - $imap_shared = $config->get('imap_ns_shared'); - $imap_delimiter = $config->get('imap_delimiter'); - - if (!$this->check_connection()) { - return; - } - - $ns = $this->conn->getNamespace(); - - // Set namespaces (NAMESPACE supported) - if (is_array($ns)) { - $this->namespace = $ns; - } - else { - $this->namespace = array( - 'personal' => NULL, - 'other' => NULL, - 'shared' => NULL, - ); - } - - if ($imap_delimiter) { - $this->delimiter = $imap_delimiter; - } - if (empty($this->delimiter)) { - $this->delimiter = $this->namespace['personal'][0][1]; - } - if (empty($this->delimiter)) { - $this->delimiter = $this->conn->getHierarchyDelimiter(); - } - if (empty($this->delimiter)) { - $this->delimiter = '/'; - } - - // Overwrite namespaces - if ($imap_personal !== null) { - $this->namespace['personal'] = NULL; - foreach ((array)$imap_personal as $dir) { - $this->namespace['personal'][] = array($dir, $this->delimiter); - } - } - if ($imap_other !== null) { - $this->namespace['other'] = NULL; - foreach ((array)$imap_other as $dir) { - if ($dir) { - $this->namespace['other'][] = array($dir, $this->delimiter); - } - } - } - if ($imap_shared !== null) { - $this->namespace['shared'] = NULL; - foreach ((array)$imap_shared as $dir) { - if ($dir) { - $this->namespace['shared'][] = array($dir, $this->delimiter); - } - } - } - - // Find personal namespace prefix for mod_folder() - // Prefix can be removed when there is only one personal namespace - if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) { - $this->namespace['prefix'] = $this->namespace['personal'][0][0]; - } - - $_SESSION['imap_namespace'] = $this->namespace; - $_SESSION['imap_delimiter'] = $this->delimiter; - } - - - /** - * Get message count for a specific folder - * - * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] - * @param boolean $force Force reading from server and update cache - * @param boolean $status Enables storing folder status info (max UID/count), - * required for folder_status() - * - * @return int Number of messages - */ - public function count($folder='', $mode='ALL', $force=false, $status=true) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - return $this->countmessages($folder, $mode, $force, $status); - } - - - /** - * protected method for getting nr of messages - * - * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] - * @param boolean $force Force reading from server and update cache - * @param boolean $status Enables storing folder status info (max UID/count), - * required for folder_status() - * - * @return int Number of messages - * @see rcube_imap::count() - */ - protected function countmessages($folder, $mode='ALL', $force=false, $status=true) - { - $mode = strtoupper($mode); - - // count search set, assume search set is always up-to-date (don't check $force flag) - if ($this->search_string && $folder == $this->folder && ($mode == 'ALL' || $mode == 'THREADS')) { - if ($mode == 'ALL') { - return $this->search_set->count_messages(); - } - else { - return $this->search_set->count(); - } - } - - $a_folder_cache = $this->get_cache('messagecount'); - - // return cached value - if (!$force && is_array($a_folder_cache[$folder]) && isset($a_folder_cache[$folder][$mode])) { - return $a_folder_cache[$folder][$mode]; - } - - if (!is_array($a_folder_cache[$folder])) { - $a_folder_cache[$folder] = array(); - } - - if ($mode == 'THREADS') { - $res = $this->fetch_threads($folder, $force); - $count = $res->count(); - - if ($status) { - $msg_count = $res->count_messages(); - $this->set_folder_stats($folder, 'cnt', $msg_count); - $this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0); - } - } - // Need connection here - else if (!$this->check_connection()) { - return 0; - } - // RECENT count is fetched a bit different - else if ($mode == 'RECENT') { - $count = $this->conn->countRecent($folder); - } - // use SEARCH for message counting - else if (!empty($this->options['skip_deleted'])) { - $search_str = "ALL UNDELETED"; - $keys = array('COUNT'); - - if ($mode == 'UNSEEN') { - $search_str .= " UNSEEN"; - } - else { - if ($this->messages_caching) { - $keys[] = 'ALL'; - } - if ($status) { - $keys[] = 'MAX'; - } - } - - // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here - - // get message count using (E)SEARCH - // not very performant but more precise (using UNDELETED) - $index = $this->conn->search($folder, $search_str, true, $keys); - $count = $index->count(); - - if ($mode == 'ALL') { - // Cache index data, will be used in index_direct() - $this->icache['undeleted_idx'] = $index; - - if ($status) { - $this->set_folder_stats($folder, 'cnt', $count); - $this->set_folder_stats($folder, 'maxuid', $index->max()); - } - } - } - else { - if ($mode == 'UNSEEN') { - $count = $this->conn->countUnseen($folder); - } - else { - $count = $this->conn->countMessages($folder); - if ($status) { - $this->set_folder_stats($folder,'cnt', $count); - $this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0); - } - } - } - - $a_folder_cache[$folder][$mode] = (int)$count; - - // write back to cache - $this->update_cache('messagecount', $a_folder_cache); - - return (int)$count; - } - - - /** - * Public method for listing headers - * - * @param string $folder Folder name - * @param int $page Current page to list - * @param string $sort_field Header field to sort by - * @param string $sort_order Sort order [ASC|DESC] - * @param int $slice Number of slice items to extract from result array - * - * @return array Indexed array with message header objects - */ - public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice); - } - - - /** - * protected method for listing message headers - * - * @param string $folder Folder name - * @param int $page Current page to list - * @param string $sort_field Header field to sort by - * @param string $sort_order Sort order [ASC|DESC] - * @param int $slice Number of slice items to extract from result array - * - * @return array Indexed array with message header objects - * @see rcube_imap::list_messages - */ - protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) - { - if (!strlen($folder)) { - return array(); - } - - $this->set_sort_order($sort_field, $sort_order); - $page = $page ? $page : $this->list_page; - - // use saved message set - if ($this->search_string && $folder == $this->folder) { - return $this->list_search_messages($folder, $page, $slice); - } - - if ($this->threading) { - return $this->list_thread_messages($folder, $page, $slice); - } - - // get UIDs of all messages in the folder, sorted - $index = $this->index($folder, $this->sort_field, $this->sort_order); - - if ($index->is_empty()) { - return array(); - } - - $from = ($page-1) * $this->page_size; - $to = $from + $this->page_size; - - $index->slice($from, $to - $from); - - if ($slice) { - $index->slice(-$slice, $slice); - } - - // fetch reqested messages headers - $a_index = $index->get(); - $a_msg_headers = $this->fetch_headers($folder, $a_index); - - return array_values($a_msg_headers); - } - - - /** - * protected method for listing message headers using threads - * - * @param string $folder Folder name - * @param int $page Current page to list - * @param int $slice Number of slice items to extract from result array - * - * @return array Indexed array with message header objects - * @see rcube_imap::list_messages - */ - protected function list_thread_messages($folder, $page, $slice=0) - { - // get all threads (not sorted) - if ($mcache = $this->get_mcache_engine()) { - $threads = $mcache->get_thread($folder); - } - else { - $threads = $this->fetch_threads($folder); - } - - return $this->fetch_thread_headers($folder, $threads, $page, $slice); - } - - /** - * Method for fetching threads data - * - * @param string $folder Folder name - * @param bool $force Use IMAP server, no cache - * - * @return rcube_imap_thread Thread data object - */ - function fetch_threads($folder, $force = false) - { - if (!$force && ($mcache = $this->get_mcache_engine())) { - // don't store in self's internal cache, cache has it's own internal cache - return $mcache->get_thread($folder); - } - - if (empty($this->icache['threads'])) { - if (!$this->check_connection()) { - return new rcube_result_thread(); - } - - // get all threads - $result = $this->conn->thread($folder, $this->threading, - $this->options['skip_deleted'] ? 'UNDELETED' : '', true); - - // add to internal (fast) cache - $this->icache['threads'] = $result; - } - - return $this->icache['threads']; - } - - - /** - * protected method for fetching threaded messages headers - * - * @param string $folder Folder name - * @param rcube_result_thread $threads Threads data object - * @param int $page List page number - * @param int $slice Number of threads to slice - * - * @return array Messages headers - */ - protected function fetch_thread_headers($folder, $threads, $page, $slice=0) - { - // Sort thread structure - $this->sort_threads($threads); - - $from = ($page-1) * $this->page_size; - $to = $from + $this->page_size; - - $threads->slice($from, $to - $from); - - if ($slice) { - $threads->slice(-$slice, $slice); - } - - // Get UIDs of all messages in all threads - $a_index = $threads->get(); - - // fetch reqested headers from server - $a_msg_headers = $this->fetch_headers($folder, $a_index); - - unset($a_index); - - // Set depth, has_children and unread_children fields in headers - $this->set_thread_flags($a_msg_headers, $threads); - - return array_values($a_msg_headers); - } - - - /** - * protected method for setting threaded messages flags: - * depth, has_children and unread_children - * - * @param array $headers Reference to headers array indexed by message UID - * @param rcube_result_thread $threads Threads data object - * - * @return array Message headers array indexed by message UID - */ - protected function set_thread_flags(&$headers, $threads) - { - $parents = array(); - - list ($msg_depth, $msg_children) = $threads->get_thread_data(); - - foreach ($headers as $uid => $header) { - $depth = $msg_depth[$uid]; - $parents = array_slice($parents, 0, $depth); - - if (!empty($parents)) { - $headers[$uid]->parent_uid = end($parents); - if (empty($header->flags['SEEN'])) - $headers[$parents[0]]->unread_children++; - } - array_push($parents, $uid); - - $headers[$uid]->depth = $depth; - $headers[$uid]->has_children = $msg_children[$uid]; - } - } - - - /** - * protected method for listing a set of message headers (search results) - * - * @param string $folder Folder name - * @param int $page Current page to list - * @param int $slice Number of slice items to extract from result array - * - * @return array Indexed array with message header objects - */ - protected function list_search_messages($folder, $page, $slice=0) - { - if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) { - return array(); - } - - // use saved messages from searching - if ($this->threading) { - return $this->list_search_thread_messages($folder, $page, $slice); - } - - // search set is threaded, we need a new one - if ($this->search_threads) { - $this->search('', $this->search_string, $this->search_charset, $this->sort_field); - } - - $index = clone $this->search_set; - $from = ($page-1) * $this->page_size; - $to = $from + $this->page_size; - - // return empty array if no messages found - if ($index->is_empty()) { - return array(); - } - - // quickest method (default sorting) - if (!$this->search_sort_field && !$this->sort_field) { - $got_index = true; - } - // sorted messages, so we can first slice array and then fetch only wanted headers - else if ($this->search_sorted) { // SORT searching result - $got_index = true; - // reset search set if sorting field has been changed - if ($this->sort_field && $this->search_sort_field != $this->sort_field) { - $this->search('', $this->search_string, $this->search_charset, $this->sort_field); - - $index = clone $this->search_set; - - // return empty array if no messages found - if ($index->is_empty()) { - return array(); - } - } - } - - if ($got_index) { - if ($this->sort_order != $index->get_parameters('ORDER')) { - $index->revert(); - } - - // get messages uids for one page - $index->slice($from, $to-$from); - - if ($slice) { - $index->slice(-$slice, $slice); - } - - // fetch headers - $a_index = $index->get(); - $a_msg_headers = $this->fetch_headers($folder, $a_index); - - return array_values($a_msg_headers); - } - - // SEARCH result, need sorting - $cnt = $index->count(); - - // 300: experimantal value for best result - if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) { - // use memory less expensive (and quick) method for big result set - $index = clone $this->index('', $this->sort_field, $this->sort_order); - // get messages uids for one page... - $index->slice($start_msg, min($cnt-$from, $this->page_size)); - - if ($slice) { - $index->slice(-$slice, $slice); - } - - // ...and fetch headers - $a_index = $index->get(); - $a_msg_headers = $this->fetch_headers($folder, $a_index); - - return array_values($a_msg_headers); - } - else { - // for small result set we can fetch all messages headers - $a_index = $index->get(); - $a_msg_headers = $this->fetch_headers($folder, $a_index, false); - - // return empty array if no messages found - if (!is_array($a_msg_headers) || empty($a_msg_headers)) { - return array(); - } - - if (!$this->check_connection()) { - return array(); - } - - // if not already sorted - $a_msg_headers = $this->conn->sortHeaders( - $a_msg_headers, $this->sort_field, $this->sort_order); - - // only return the requested part of the set - $slice_length = min($this->page_size, $cnt - ($to > $cnt ? $from : $to)); - $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length); - - if ($slice) { - $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice); - } - - return $a_msg_headers; - } - } - - - /** - * protected method for listing a set of threaded message headers (search results) - * - * @param string $folder Folder name - * @param int $page Current page to list - * @param int $slice Number of slice items to extract from result array - * - * @return array Indexed array with message header objects - * @see rcube_imap::list_search_messages() - */ - protected function list_search_thread_messages($folder, $page, $slice=0) - { - // update search_set if previous data was fetched with disabled threading - if (!$this->search_threads) { - if ($this->search_set->is_empty()) { - return array(); - } - $this->search('', $this->search_string, $this->search_charset, $this->sort_field); - } - - return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice); - } - - - /** - * Fetches messages headers (by UID) - * - * @param string $folder Folder name - * @param array $msgs Message UIDs - * @param bool $sort Enables result sorting by $msgs - * @param bool $force Disables cache use - * - * @return array Messages headers indexed by UID - */ - function fetch_headers($folder, $msgs, $sort = true, $force = false) - { - if (empty($msgs)) { - return array(); - } - - if (!$force && ($mcache = $this->get_mcache_engine())) { - $headers = $mcache->get_messages($folder, $msgs); - } - else if (!$this->check_connection()) { - return array(); - } - else { - // fetch reqested headers from server - $headers = $this->conn->fetchHeaders( - $folder, $msgs, true, false, $this->get_fetch_headers()); - } - - if (empty($headers)) { - return array(); - } - - foreach ($headers as $h) { - $a_msg_headers[$h->uid] = $h; - } - - if ($sort) { - // use this class for message sorting - $sorter = new rcube_message_header_sorter(); - $sorter->set_index($msgs); - $sorter->sort_headers($a_msg_headers); - } - - return $a_msg_headers; - } - - - /** - * Returns current status of folder - * - * We compare the maximum UID to determine the number of - * new messages because the RECENT flag is not reliable. - * - * @param string $folder Folder name - * - * @return int Folder status - */ - public function folder_status($folder = null) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - $old = $this->get_folder_stats($folder); - - // refresh message count -> will update - $this->countmessages($folder, 'ALL', true); - - $result = 0; - - if (empty($old)) { - return $result; - } - - $new = $this->get_folder_stats($folder); - - // got new messages - if ($new['maxuid'] > $old['maxuid']) { - $result += 1; - } - // some messages has been deleted - if ($new['cnt'] < $old['cnt']) { - $result += 2; - } - - // @TODO: optional checking for messages flags changes (?) - // @TODO: UIDVALIDITY checking - - return $result; - } - - - /** - * Stores folder statistic data in session - * @TODO: move to separate DB table (cache?) - * - * @param string $folder Folder name - * @param string $name Data name - * @param mixed $data Data value - */ - protected function set_folder_stats($folder, $name, $data) - { - $_SESSION['folders'][$folder][$name] = $data; - } - - - /** - * Gets folder statistic data - * - * @param string $folder Folder name - * - * @return array Stats data - */ - protected function get_folder_stats($folder) - { - if ($_SESSION['folders'][$folder]) { - return (array) $_SESSION['folders'][$folder]; - } - - return array(); - } - - - /** - * Return sorted list of message UIDs - * - * @param string $folder Folder to get index from - * @param string $sort_field Sort column - * @param string $sort_order Sort order [ASC, DESC] - * - * @return rcube_result_index|rcube_result_thread List of messages (UIDs) - */ - public function index($folder = '', $sort_field = NULL, $sort_order = NULL) - { - if ($this->threading) { - return $this->thread_index($folder, $sort_field, $sort_order); - } - - $this->set_sort_order($sort_field, $sort_order); - - if (!strlen($folder)) { - $folder = $this->folder; - } - - // we have a saved search result, get index from there - if ($this->search_string) { - if ($this->search_threads) { - $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field); - } - - // use message index sort as default sorting - if (!$this->sort_field || $this->search_sorted) { - if ($this->sort_field && $this->search_sort_field != $this->sort_field) { - $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field); - } - $index = $this->search_set; - } - else if (!$this->check_connection()) { - return new rcube_result_index(); - } - else { - $index = $this->conn->index($folder, $this->search_set->get(), - $this->sort_field, $this->options['skip_deleted'], true, true); - } - - if ($this->sort_order != $index->get_parameters('ORDER')) { - $index->revert(); - } - - return $index; - } - - // check local cache - if ($mcache = $this->get_mcache_engine()) { - $index = $mcache->get_index($folder, $this->sort_field, $this->sort_order); - } - // fetch from IMAP server - else { - $index = $this->index_direct( - $folder, $this->sort_field, $this->sort_order); - } - - return $index; - } - - - /** - * Return sorted list of message UIDs ignoring current search settings. - * Doesn't uses cache by default. - * - * @param string $folder Folder to get index from - * @param string $sort_field Sort column - * @param string $sort_order Sort order [ASC, DESC] - * @param bool $skip_cache Disables cache usage - * - * @return rcube_result_index Sorted list of message UIDs - */ - public function index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true) - { - if (!$skip_cache && ($mcache = $this->get_mcache_engine())) { - $index = $mcache->get_index($folder, $sort_field, $sort_order); - } - // use message index sort as default sorting - else if (!$sort_field) { - // use search result from count() if possible - if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx']) - && $this->icache['undeleted_idx']->get_parameters('ALL') !== null - && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder - ) { - $index = $this->icache['undeleted_idx']; - } - else if (!$this->check_connection()) { - return new rcube_result_index(); - } - else { - $index = $this->conn->search($folder, - 'ALL' .($this->options['skip_deleted'] ? ' UNDELETED' : ''), true); - } - } - else if (!$this->check_connection()) { - return new rcube_result_index(); - } - // fetch complete message index - else { - if ($this->get_capability('SORT')) { - $index = $this->conn->sort($folder, $sort_field, - $this->options['skip_deleted'] ? 'UNDELETED' : '', true); - } - - if (empty($index) || $index->is_error()) { - $index = $this->conn->index($folder, "1:*", $sort_field, - $this->options['skip_deleted'], false, true); - } - } - - if ($sort_order != $index->get_parameters('ORDER')) { - $index->revert(); - } - - return $index; - } - - - /** - * Return index of threaded message UIDs - * - * @param string $folder Folder to get index from - * @param string $sort_field Sort column - * @param string $sort_order Sort order [ASC, DESC] - * - * @return rcube_result_thread Message UIDs - */ - public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - // we have a saved search result, get index from there - if ($this->search_string && $this->search_threads && $folder == $this->folder) { - $threads = $this->search_set; - } - else { - // get all threads (default sort order) - $threads = $this->fetch_threads($folder); - } - - $this->set_sort_order($sort_field, $sort_order); - $this->sort_threads($threads); - - return $threads; - } - - - /** - * Sort threaded result, using THREAD=REFS method - * - * @param rcube_result_thread $threads Threads result set - */ - protected function sort_threads($threads) - { - if ($threads->is_empty()) { - return; - } - - // THREAD=ORDEREDSUBJECT: sorting by sent date of root message - // THREAD=REFERENCES: sorting by sent date of root message - // THREAD=REFS: sorting by the most recent date in each thread - - if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) { - $index = $this->index_direct($this->folder, $this->sort_field, $this->sort_order, false); - - if (!$index->is_empty()) { - $threads->sort($index); - } - } - else { - if ($this->sort_order != $threads->get_parameters('ORDER')) { - $threads->revert(); - } - } - } - - - /** - * Invoke search request to IMAP server - * - * @param string $folder Folder name to search in - * @param string $str Search criteria - * @param string $charset Search charset - * @param string $sort_field Header field to sort by - * - * @todo: Search criteria should be provided in non-IMAP format, eg. array - */ - public function search($folder='', $str='ALL', $charset=NULL, $sort_field=NULL) - { - if (!$str) { - $str = 'ALL'; - } - - if (!strlen($folder)) { - $folder = $this->folder; - } - - $results = $this->search_index($folder, $str, $charset, $sort_field); - - $this->set_search_set(array($str, $results, $charset, $sort_field, - $this->threading || $this->search_sorted ? true : false)); - } - - - /** - * Direct (real and simple) SEARCH request (without result sorting and caching). - * - * @param string $mailbox Mailbox name to search in - * @param string $str Search string - * - * @return rcube_result_index Search result (UIDs) - */ - public function search_once($folder = null, $str = 'ALL') - { - if (!$str) { - return 'ALL'; - } - - if (!strlen($folder)) { - $folder = $this->folder; - } - - if (!$this->check_connection()) { - return new rcube_result_index(); - } - - $index = $this->conn->search($folder, $str, true); - - return $index; - } - - - /** - * protected search method - * - * @param string $folder Folder name - * @param string $criteria Search criteria - * @param string $charset Charset - * @param string $sort_field Sorting field - * - * @return rcube_result_index|rcube_result_thread Search results (UIDs) - * @see rcube_imap::search() - */ - protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL) - { - $orig_criteria = $criteria; - - if (!$this->check_connection()) { - if ($this->threading) { - return new rcube_result_thread(); - } - else { - return new rcube_result_index(); - } - } - - if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) { - $criteria = 'UNDELETED '.$criteria; - } - - // unset CHARSET if criteria string is ASCII, this way - // SEARCH won't be re-sent after "unsupported charset" response - if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) { - $charset = 'US-ASCII'; - } - - if ($this->threading) { - $threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset); - - // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8, - // but I've seen that Courier doesn't support UTF-8) - if ($threads->is_error() && $charset && $charset != 'US-ASCII') { - $threads = $this->conn->thread($folder, $this->threading, - $this->convert_criteria($criteria, $charset), true, 'US-ASCII'); - } - - return $threads; - } - - if ($sort_field && $this->get_capability('SORT')) { - $charset = $charset ? $charset : $this->default_charset; - $messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset); - - // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8, - // but I've seen Courier with disabled UTF-8 support) - if ($messages->is_error() && $charset && $charset != 'US-ASCII') { - $messages = $this->conn->sort($folder, $sort_field, - $this->convert_criteria($criteria, $charset), true, 'US-ASCII'); - } - - if (!$messages->is_error()) { - $this->search_sorted = true; - return $messages; - } - } - - $messages = $this->conn->search($folder, - ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true); - - // Error, try with US-ASCII (some servers may support only US-ASCII) - if ($messages->is_error() && $charset && $charset != 'US-ASCII') { - $messages = $this->conn->search($folder, - $this->convert_criteria($criteria, $charset), true); - } - - $this->search_sorted = false; - - return $messages; - } - - - /** - * Converts charset of search criteria string - * - * @param string $str Search string - * @param string $charset Original charset - * @param string $dest_charset Destination charset (default US-ASCII) - * - * @return string Search string - */ - protected function convert_criteria($str, $charset, $dest_charset='US-ASCII') - { - // convert strings to US_ASCII - if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) { - $last = 0; $res = ''; - foreach ($matches[1] as $m) { - $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n - $string = substr($str, $string_offset - 1, $m[0]); - $string = rcube_charset::convert($string, $charset, $dest_charset); - if ($string === false) { - continue; - } - $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string); - $last = $m[0] + $string_offset - 1; - } - if ($last < strlen($str)) { - $res .= substr($str, $last, strlen($str)-$last); - } - } - // strings for conversion not found - else { - $res = $str; - } - - return $res; - } - - - /** - * Refresh saved search set - * - * @return array Current search set - */ - public function refresh_search() - { - if (!empty($this->search_string)) { - $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field); - } - - return $this->get_search_set(); - } - - - /** - * Return message headers object of a specific message - * - * @param int $id Message UID - * @param string $folder Folder to read from - * @param bool $force True to skip cache - * - * @return rcube_message_header Message headers - */ - public function get_message_headers($uid, $folder = null, $force = false) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - // get cached headers - if (!$force && $uid && ($mcache = $this->get_mcache_engine())) { - $headers = $mcache->get_message($folder, $uid); - } - else if (!$this->check_connection()) { - $headers = false; - } - else { - $headers = $this->conn->fetchHeader( - $folder, $uid, true, true, $this->get_fetch_headers()); - } - - return $headers; - } - - - /** - * Fetch message headers and body structure from the IMAP server and build - * an object structure similar to the one generated by PEAR::Mail_mimeDecode - * - * @param int $uid Message UID to fetch - * @param string $folder Folder to read from - * - * @return object rcube_message_header Message data - */ - public function get_message($uid, $folder = null) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - // Check internal cache - if (!empty($this->icache['message'])) { - if (($headers = $this->icache['message']) && $headers->uid == $uid) { - return $headers; - } - } - - $headers = $this->get_message_headers($uid, $folder); - - // message doesn't exist? - if (empty($headers)) { - return null; - } - - // structure might be cached - if (!empty($headers->structure)) { - return $headers; - } - - $this->msg_uid = $uid; - - if (!$this->check_connection()) { - return $headers; - } - - if (empty($headers->bodystructure)) { - $headers->bodystructure = $this->conn->getStructure($folder, $uid, true); - } - - $structure = $headers->bodystructure; - - if (empty($structure)) { - return $headers; - } - - // set message charset from message headers - if ($headers->charset) { - $this->struct_charset = $headers->charset; - } - else { - $this->struct_charset = $this->structure_charset($structure); - } - - $headers->ctype = strtolower($headers->ctype); - - // Here we can recognize malformed BODYSTRUCTURE and - // 1. [@TODO] parse the message in other way to create our own message structure - // 2. or just show the raw message body. - // Example of structure for malformed MIME message: - // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL) - if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain' - && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') { - // we can handle single-part messages, by simple fix in structure (#1486898) - if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) { - $structure[0] = $m[1]; - $structure[1] = $m[2]; - } - else { - // Try to parse the message using Mail_mimeDecode package - // We need a better solution, Mail_mimeDecode parses message - // in memory, which wouldn't work for very big messages, - // (it uses up to 10x more memory than the message size) - // it's also buggy and not actively developed - if ($headers->size && rcube_utils::mem_check($headers->size * 10)) { - $raw_msg = $this->get_raw_body($uid); - $struct = rcube_mime::parse_message($raw_msg); - } - else { - return $headers; - } - } - } - - if (empty($struct)) { - $struct = $this->structure_part($structure, 0, '', $headers); - } - - // don't trust given content-type - if (empty($struct->parts) && !empty($headers->ctype)) { - $struct->mime_id = '1'; - $struct->mimetype = strtolower($headers->ctype); - list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype); - } - - $headers->structure = $struct; - - return $this->icache['message'] = $headers; - } - - - /** - * Build message part object - * - * @param array $part - * @param int $count - * @param string $parent - */ - protected function structure_part($part, $count=0, $parent='', $mime_headers=null) - { - $struct = new rcube_message_part; - $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count"; - - // multipart - if (is_array($part[0])) { - $struct->ctype_primary = 'multipart'; - - /* RFC3501: BODYSTRUCTURE fields of multipart part - part1 array - part2 array - part3 array - .... - 1. subtype - 2. parameters (optional) - 3. description (optional) - 4. language (optional) - 5. location (optional) - */ - - // find first non-array entry - for ($i=1; $i<count($part); $i++) { - if (!is_array($part[$i])) { - $struct->ctype_secondary = strtolower($part[$i]); - break; - } - } - - $struct->mimetype = 'multipart/'.$struct->ctype_secondary; - - // build parts list for headers pre-fetching - for ($i=0; $i<count($part); $i++) { - if (!is_array($part[$i])) { - break; - } - // fetch message headers if message/rfc822 - // or named part (could contain Content-Location header) - if (!is_array($part[$i][0])) { - $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1; - if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') { - $mime_part_headers[] = $tmp_part_id; - } - else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) { - $mime_part_headers[] = $tmp_part_id; - } - } - } - - // pre-fetch headers of all parts (in one command for better performance) - // @TODO: we could do this before _structure_part() call, to fetch - // headers for parts on all levels - if ($mime_part_headers) { - $mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder, - $this->msg_uid, $mime_part_headers); - } - - $struct->parts = array(); - for ($i=0, $count=0; $i<count($part); $i++) { - if (!is_array($part[$i])) { - break; - } - $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1; - $struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id, - $mime_part_headers[$tmp_part_id]); - } - - return $struct; - } - - /* RFC3501: BODYSTRUCTURE fields of non-multipart part - 0. type - 1. subtype - 2. parameters - 3. id - 4. description - 5. encoding - 6. size - -- text - 7. lines - -- message/rfc822 - 7. envelope structure - 8. body structure - 9. lines - -- - x. md5 (optional) - x. disposition (optional) - x. language (optional) - x. location (optional) - */ - - // regular part - $struct->ctype_primary = strtolower($part[0]); - $struct->ctype_secondary = strtolower($part[1]); - $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary; - - // read content type parameters - if (is_array($part[2])) { - $struct->ctype_parameters = array(); - for ($i=0; $i<count($part[2]); $i+=2) { - $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1]; - } - - if (isset($struct->ctype_parameters['charset'])) { - $struct->charset = $struct->ctype_parameters['charset']; - } - } - - // #1487700: workaround for lack of charset in malformed structure - if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) { - $struct->charset = $mime_headers->charset; - } - - // read content encoding - if (!empty($part[5])) { - $struct->encoding = strtolower($part[5]); - $struct->headers['content-transfer-encoding'] = $struct->encoding; - } - - // get part size - if (!empty($part[6])) { - $struct->size = intval($part[6]); - } - - // read part disposition - $di = 8; - if ($struct->ctype_primary == 'text') { - $di += 1; - } - else if ($struct->mimetype == 'message/rfc822') { - $di += 3; - } - - if (is_array($part[$di]) && count($part[$di]) == 2) { - $struct->disposition = strtolower($part[$di][0]); - - if (is_array($part[$di][1])) { - for ($n=0; $n<count($part[$di][1]); $n+=2) { - $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1]; - } - } - } - - // get message/rfc822's child-parts - if (is_array($part[8]) && $di != 8) { - $struct->parts = array(); - for ($i=0, $count=0; $i<count($part[8]); $i++) { - if (!is_array($part[8][$i])) { - break; - } - $struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id); - } - } - - // get part ID - if (!empty($part[3])) { - $struct->content_id = $part[3]; - $struct->headers['content-id'] = $part[3]; - - if (empty($struct->disposition)) { - $struct->disposition = 'inline'; - } - } - - // fetch message headers if message/rfc822 or named part (could contain Content-Location header) - if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) { - if (empty($mime_headers)) { - $mime_headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $struct->mime_id); - } - - if (is_string($mime_headers)) { - $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers; - } - else if (is_object($mime_headers)) { - $struct->headers = get_object_vars($mime_headers) + $struct->headers; - } - - // get real content-type of message/rfc822 - if ($struct->mimetype == 'message/rfc822') { - // single-part - if (!is_array($part[8][0])) { - $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]); - } - // multi-part - else { - for ($n=0; $n<count($part[8]); $n++) { - if (!is_array($part[8][$n])) { - break; - } - } - $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]); - } - } - - if ($struct->ctype_primary == 'message' && empty($struct->parts)) { - if (is_array($part[8]) && $di != 8) { - $struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id); - } - } - } - - // normalize filename property - $this->set_part_filename($struct, $mime_headers); - - return $struct; - } - - - /** - * Set attachment filename from message part structure - * - * @param rcube_message_part $part Part object - * @param string $headers Part's raw headers - */ - protected function set_part_filename(&$part, $headers=null) - { - if (!empty($part->d_parameters['filename'])) { - $filename_mime = $part->d_parameters['filename']; - } - else if (!empty($part->d_parameters['filename*'])) { - $filename_encoded = $part->d_parameters['filename*']; - } - else if (!empty($part->ctype_parameters['name*'])) { - $filename_encoded = $part->ctype_parameters['name*']; - } - // RFC2231 value continuations - // TODO: this should be rewrited to support RFC2231 4.1 combinations - else if (!empty($part->d_parameters['filename*0'])) { - $i = 0; - while (isset($part->d_parameters['filename*'.$i])) { - $filename_mime .= $part->d_parameters['filename*'.$i]; - $i++; - } - // some servers (eg. dovecot-1.x) have no support for parameter value continuations - // we must fetch and parse headers "manually" - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_mime = ''; - $i = 0; - while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_mime .= $matches[1]; - $i++; - } - } - } - else if (!empty($part->d_parameters['filename*0*'])) { - $i = 0; - while (isset($part->d_parameters['filename*'.$i.'*'])) { - $filename_encoded .= $part->d_parameters['filename*'.$i.'*']; - $i++; - } - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_encoded = ''; - $i = 0; $matches = array(); - while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_encoded .= $matches[1]; - $i++; - } - } - } - else if (!empty($part->ctype_parameters['name*0'])) { - $i = 0; - while (isset($part->ctype_parameters['name*'.$i])) { - $filename_mime .= $part->ctype_parameters['name*'.$i]; - $i++; - } - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_mime = ''; - $i = 0; $matches = array(); - while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_mime .= $matches[1]; - $i++; - } - } - } - else if (!empty($part->ctype_parameters['name*0*'])) { - $i = 0; - while (isset($part->ctype_parameters['name*'.$i.'*'])) { - $filename_encoded .= $part->ctype_parameters['name*'.$i.'*']; - $i++; - } - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_encoded = ''; - $i = 0; $matches = array(); - while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_encoded .= $matches[1]; - $i++; - } - } - } - // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird) - else if (!empty($part->ctype_parameters['name'])) { - $filename_mime = $part->ctype_parameters['name']; - } - // Content-Disposition - else if (!empty($part->headers['content-description'])) { - $filename_mime = $part->headers['content-description']; - } - else { - return; - } - - // decode filename - if (!empty($filename_mime)) { - if (!empty($part->charset)) { - $charset = $part->charset; - } - else if (!empty($this->struct_charset)) { - $charset = $this->struct_charset; - } - else { - $charset = rcube_charset::detect($filename_mime, $this->default_charset); - } - - $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset); - } - else if (!empty($filename_encoded)) { - // decode filename according to RFC 2231, Section 4 - if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) { - $filename_charset = $fmatches[1]; - $filename_encoded = $fmatches[2]; - } - - $part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset); - } - } - - - /** - * Get charset name from message structure (first part) - * - * @param array $structure Message structure - * - * @return string Charset name - */ - protected function structure_charset($structure) - { - while (is_array($structure)) { - if (is_array($structure[2]) && $structure[2][0] == 'charset') { - return $structure[2][1]; - } - $structure = $structure[0]; - } - } - - - /** - * Fetch message body of a specific message from the server - * - * @param int $uid Message UID - * @param string $part Part number - * @param rcube_message_part $o_part Part object created by get_structure() - * @param mixed $print True to print part, ressource to write part contents in - * @param resource $fp File pointer to save the message part - * @param boolean $skip_charset_conv Disables charset conversion - * - * @return string Message/part body if not printed - */ - public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false) - { - if (!$this->check_connection()) { - return null; - } - - // get part data if not provided - if (!is_object($o_part)) { - $structure = $this->conn->getStructure($this->folder, $uid, true); - $part_data = rcube_imap_generic::getStructurePartData($structure, $part); - - $o_part = new rcube_message_part; - $o_part->ctype_primary = $part_data['type']; - $o_part->encoding = $part_data['encoding']; - $o_part->charset = $part_data['charset']; - $o_part->size = $part_data['size']; - } - - if ($o_part && $o_part->size) { - $body = $this->conn->handlePartBody($this->folder, $uid, true, - $part ? $part : 'TEXT', $o_part->encoding, $print, $fp); - } - - if ($fp || $print) { - return true; - } - - // convert charset (if text or message part) - if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) { - // Remove NULL characters if any (#1486189) - if (strpos($body, "\x00") !== false) { - $body = str_replace("\x00", '', $body); - } - - if (!$skip_charset_conv) { - if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') { - // try to extract charset information from HTML meta tag (#1488125) - if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) { - $o_part->charset = strtoupper($m[1]); - } - else { - $o_part->charset = $this->default_charset; - } - } - $body = rcube_charset::convert($body, $o_part->charset); - } - } - - return $body; - } - - - /** - * Returns the whole message source as string (or saves to a file) - * - * @param int $uid Message UID - * @param resource $fp File pointer to save the message - * - * @return string Message source string - */ - public function get_raw_body($uid, $fp=null) - { - if (!$this->check_connection()) { - return null; - } - - return $this->conn->handlePartBody($this->folder, $uid, - true, null, null, false, $fp); - } - - - /** - * Returns the message headers as string - * - * @param int $uid Message UID - * - * @return string Message headers string - */ - public function get_raw_headers($uid) - { - if (!$this->check_connection()) { - return null; - } - - return $this->conn->fetchPartHeader($this->folder, $uid, true); - } - - - /** - * Sends the whole message source to stdout - * - * @param int $uid Message UID - * @param bool $formatted Enables line-ending formatting - */ - public function print_raw_body($uid, $formatted = true) - { - if (!$this->check_connection()) { - return; - } - - $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted); - } - - - /** - * Set message flag to one or several messages - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT - * @param string $folder Folder name - * @param boolean $skip_cache True to skip message cache clean up - * - * @return boolean Operation status - */ - public function set_flag($uids, $flag, $folder=null, $skip_cache=false) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - if (!$this->check_connection()) { - return false; - } - - $flag = strtoupper($flag); - list($uids, $all_mode) = $this->parse_uids($uids); - - if (strpos($flag, 'UN') === 0) { - $result = $this->conn->unflag($folder, $uids, substr($flag, 2)); - } - else { - $result = $this->conn->flag($folder, $uids, $flag); - } - - if ($result) { - // reload message headers if cached - // @TODO: update flags instead removing from cache - if (!$skip_cache && ($mcache = $this->get_mcache_engine())) { - $status = strpos($flag, 'UN') !== 0; - $mflag = preg_replace('/^UN/', '', $flag); - $mcache->change_flag($folder, $all_mode ? null : explode(',', $uids), - $mflag, $status); - } - - // clear cached counters - if ($flag == 'SEEN' || $flag == 'UNSEEN') { - $this->clear_messagecount($folder, 'SEEN'); - $this->clear_messagecount($folder, 'UNSEEN'); - } - else if ($flag == 'DELETED') { - $this->clear_messagecount($folder, 'DELETED'); - } - } - - return $result; - } - - - /** - * Append a mail message (source) to a specific folder - * - * @param string $folder Target folder - * @param string $message The message source string or filename - * @param string $headers Headers string if $message contains only the body - * @param boolean $is_file True if $message is a filename - * @param array $flags Message flags - * @param mixed $date Message internal date - * - * @return int|bool Appended message UID or True on success, False on error - */ - public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - if (!$this->check_connection()) { - return false; - } - - // make sure folder exists - if (!$this->folder_exists($folder)) { - return false; - } - - $date = $this->date_format($date); - - if ($is_file) { - $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date); - } - else { - $saved = $this->conn->append($folder, $message, $flags, $date); - } - - if ($saved) { - // increase messagecount of the target folder - $this->set_messagecount($folder, 'ALL', 1); - } - - return $saved; - } - - - /** - * Move a message from one folder to another - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $to_mbox Target folder - * @param string $from_mbox Source folder - * - * @return boolean True on success, False on error - */ - public function move_message($uids, $to_mbox, $from_mbox='') - { - if (!strlen($from_mbox)) { - $from_mbox = $this->folder; - } - - if ($to_mbox === $from_mbox) { - return false; - } - - list($uids, $all_mode) = $this->parse_uids($uids); - - // exit if no message uids are specified - if (empty($uids)) { - return false; - } - - if (!$this->check_connection()) { - return false; - } - - // make sure folder exists - if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) { - if (in_array($to_mbox, $this->default_folders)) { - if (!$this->create_folder($to_mbox, true)) { - return false; - } - } - else { - return false; - } - } - - $config = rcube::get_instance()->config; - $to_trash = $to_mbox == $config->get('trash_mbox'); - - // flag messages as read before moving them - if ($to_trash && $config->get('read_when_deleted')) { - // don't flush cache (4th argument) - $this->set_flag($uids, 'SEEN', $from_mbox, true); - } - - // move messages - $moved = $this->conn->move($uids, $from_mbox, $to_mbox); - - // send expunge command in order to have the moved message - // really deleted from the source folder - if ($moved) { - $this->expunge_message($uids, $from_mbox, false); - $this->clear_messagecount($from_mbox); - $this->clear_messagecount($to_mbox); - } - // moving failed - else if ($to_trash && $config->get('delete_always', false)) { - $moved = $this->delete_message($uids, $from_mbox); - } - - if ($moved) { - // unset threads internal cache - unset($this->icache['threads']); - - // remove message ids from search set - if ($this->search_set && $from_mbox == $this->folder) { - // threads are too complicated to just remove messages from set - if ($this->search_threads || $all_mode) { - $this->refresh_search(); - } - else { - $this->search_set->filter(explode(',', $uids)); - } - } - - // remove cached messages - // @TODO: do cache update instead of clearing it - $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids)); - } - - return $moved; - } - - - /** - * Copy a message from one folder to another - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $to_mbox Target folder - * @param string $from_mbox Source folder - * - * @return boolean True on success, False on error - */ - public function copy_message($uids, $to_mbox, $from_mbox='') - { - if (!strlen($from_mbox)) { - $from_mbox = $this->folder; - } - - list($uids, $all_mode) = $this->parse_uids($uids); - - // exit if no message uids are specified - if (empty($uids)) { - return false; - } - - if (!$this->check_connection()) { - return false; - } - - // make sure folder exists - if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) { - if (in_array($to_mbox, $this->default_folders)) { - if (!$this->create_folder($to_mbox, true)) { - return false; - } - } - else { - return false; - } - } - - // copy messages - $copied = $this->conn->copy($uids, $from_mbox, $to_mbox); - - if ($copied) { - $this->clear_messagecount($to_mbox); - } - - return $copied; - } - - - /** - * Mark messages as deleted and expunge them - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $folder Source folder - * - * @return boolean True on success, False on error - */ - public function delete_message($uids, $folder='') - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - list($uids, $all_mode) = $this->parse_uids($uids); - - // exit if no message uids are specified - if (empty($uids)) { - return false; - } - - if (!$this->check_connection()) { - return false; - } - - $deleted = $this->conn->flag($folder, $uids, 'DELETED'); - - if ($deleted) { - // send expunge command in order to have the deleted message - // really deleted from the folder - $this->expunge_message($uids, $folder, false); - $this->clear_messagecount($folder); - unset($this->uid_id_map[$folder]); - - // unset threads internal cache - unset($this->icache['threads']); - - // remove message ids from search set - if ($this->search_set && $folder == $this->folder) { - // threads are too complicated to just remove messages from set - if ($this->search_threads || $all_mode) { - $this->refresh_search(); - } - else { - $this->search_set->filter(explode(',', $uids)); - } - } - - // remove cached messages - $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids)); - } - - return $deleted; - } - - - /** - * Send IMAP expunge command and clear cache - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $folder Folder name - * @param boolean $clear_cache False if cache should not be cleared - * - * @return boolean True on success, False on failure - */ - public function expunge_message($uids, $folder = null, $clear_cache = true) - { - if ($uids && $this->get_capability('UIDPLUS')) { - list($uids, $all_mode) = $this->parse_uids($uids); - } - else { - $uids = null; - } - - if (!strlen($folder)) { - $folder = $this->folder; - } - - if (!$this->check_connection()) { - return false; - } - - // force folder selection and check if folder is writeable - // to prevent a situation when CLOSE is executed on closed - // or EXPUNGE on read-only folder - $result = $this->conn->select($folder); - if (!$result) { - return false; - } - - if (!$this->conn->data['READ-WRITE']) { - $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only"); - return false; - } - - // CLOSE(+SELECT) should be faster than EXPUNGE - if (empty($uids) || $all_mode) { - $result = $this->conn->close(); - } - else { - $result = $this->conn->expunge($folder, $uids); - } - - if ($result && $clear_cache) { - $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids)); - $this->clear_messagecount($folder); - } - - return $result; - } - - - /* -------------------------------- - * folder managment - * --------------------------------*/ - - /** - * Public method for listing subscribed folders. - * - * @param string $root Optional root folder - * @param string $name Optional name pattern - * @param string $filter Optional filter - * @param string $rights Optional ACL requirements - * @param bool $skip_sort Enable to return unsorted list (for better performance) - * - * @return array List of folders - */ - public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) - { - $cache_key = $root.':'.$name; - if (!empty($filter)) { - $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter)); - } - $cache_key .= ':'.$rights; - $cache_key = 'mailboxes.'.md5($cache_key); - - // get cached folder list - $a_mboxes = $this->get_cache($cache_key); - if (is_array($a_mboxes)) { - return $a_mboxes; - } - - // Give plugins a chance to provide a list of folders - $data = rcube::get_instance()->plugins->exec_hook('storage_folders', - array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB')); - - if (isset($data['folders'])) { - $a_mboxes = $data['folders']; - } - else { - $a_mboxes = $this->list_folders_subscribed_direct($root, $name); - } - - if (!is_array($a_mboxes)) { - return array(); - } - - // filter folders list according to rights requirements - if ($rights && $this->get_capability('ACL')) { - $a_mboxes = $this->filter_rights($a_mboxes, $rights); - } - - // INBOX should always be available - if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) { - array_unshift($a_mboxes, 'INBOX'); - } - - // sort folders (always sort for cache) - if (!$skip_sort || $this->cache) { - $a_mboxes = $this->sort_folder_list($a_mboxes); - } - - // write folders list to cache - $this->update_cache($cache_key, $a_mboxes); - - return $a_mboxes; - } - - - /** - * Method for direct folders listing (LSUB) - * - * @param string $root Optional root folder - * @param string $name Optional name pattern - * - * @return array List of subscribed folders - * @see rcube_imap::list_folders_subscribed() - */ - public function list_folders_subscribed_direct($root='', $name='*') - { - if (!$this->check_connection()) { - return null; - } - - $config = rcube::get_instance()->config; - - // Server supports LIST-EXTENDED, we can use selection options - // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED - $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED'); - if ($list_extended) { - // This will also set folder options, LSUB doesn't do that - $a_folders = $this->conn->listMailboxes($root, $name, - NULL, array('SUBSCRIBED')); - } - else { - // retrieve list of folders from IMAP server using LSUB - $a_folders = $this->conn->listSubscribed($root, $name); - } - - if (!is_array($a_folders)) { - return array(); - } - - // #1486796: some server configurations doesn't return folders in all namespaces - if ($root == '' && $name == '*' && $config->get('imap_force_ns')) { - $this->list_folders_update($a_folders, ($list_extended ? 'ext-' : '') . 'subscribed'); - } - - if ($list_extended) { - // unsubscribe non-existent folders, remove from the list - // we can do this only when LIST response is available - if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { - foreach ($a_folders as $idx => $folder) { - if (($opts = $this->conn->data['LIST'][$folder]) - && in_array('\\NonExistent', $opts) - ) { - $this->conn->unsubscribe($folder); - unset($a_folders[$idx]); - } - } - } - } - else { - // unsubscribe non-existent folders, remove them from the list, - // we can do this only when LIST response is available - if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) { - foreach ($a_folders as $idx => $folder) { - if (!isset($this->conn->data['LIST'][$folder]) - || in_array('\\Noselect', $this->conn->data['LIST'][$folder]) - ) { - // Some servers returns \Noselect for existing folders - if (!$this->folder_exists($folder)) { - $this->conn->unsubscribe($folder); - unset($a_folders[$idx]); - } - } - } - } - } - - return $a_folders; - } - - - /** - * Get a list of all folders available on the server - * - * @param string $root IMAP root dir - * @param string $name Optional name pattern - * @param mixed $filter Optional filter - * @param string $rights Optional ACL requirements - * @param bool $skip_sort Enable to return unsorted list (for better performance) - * - * @return array Indexed array with folder names - */ - public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) - { - $cache_key = $root.':'.$name; - if (!empty($filter)) { - $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter)); - } - $cache_key .= ':'.$rights; - $cache_key = 'mailboxes.list.'.md5($cache_key); - - // get cached folder list - $a_mboxes = $this->get_cache($cache_key); - if (is_array($a_mboxes)) { - return $a_mboxes; - } - - // Give plugins a chance to provide a list of folders - $data = rcube::get_instance()->plugins->exec_hook('storage_folders', - array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST')); - - if (isset($data['folders'])) { - $a_mboxes = $data['folders']; - } - else { - // retrieve list of folders from IMAP server - $a_mboxes = $this->list_folders_direct($root, $name); - } - - if (!is_array($a_mboxes)) { - $a_mboxes = array(); - } - - // INBOX should always be available - if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) { - array_unshift($a_mboxes, 'INBOX'); - } - - // cache folder attributes - if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) { - $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']); - } - - // filter folders list according to rights requirements - if ($rights && $this->get_capability('ACL')) { - $a_folders = $this->filter_rights($a_folders, $rights); - } - - // filter folders and sort them - if (!$skip_sort) { - $a_mboxes = $this->sort_folder_list($a_mboxes); - } - - // write folders list to cache - $this->update_cache($cache_key, $a_mboxes); - - return $a_mboxes; - } - - - /** - * Method for direct folders listing (LIST) - * - * @param string $root Optional root folder - * @param string $name Optional name pattern - * - * @return array List of folders - * @see rcube_imap::list_folders() - */ - public function list_folders_direct($root='', $name='*') - { - if (!$this->check_connection()) { - return null; - } - - $result = $this->conn->listMailboxes($root, $name); - - if (!is_array($result)) { - return array(); - } - - $config = rcube::get_instance()->config; - - // #1486796: some server configurations doesn't return folders in all namespaces - if ($root == '' && $name == '*' && $config->get('imap_force_ns')) { - $this->list_folders_update($result); - } - - return $result; - } - - - /** - * Fix folders list by adding folders from other namespaces. - * Needed on some servers eg. Courier IMAP - * - * @param array $result Reference to folders list - * @param string $type Listing type (ext-subscribed, subscribed or all) - */ - private function list_folders_update(&$result, $type = null) - { - $delim = $this->get_hierarchy_delimiter(); - $namespace = $this->get_namespace(); - $search = array(); - - // build list of namespace prefixes - foreach ((array)$namespace as $ns) { - if (is_array($ns)) { - foreach ($ns as $ns_data) { - if (strlen($ns_data[0])) { - $search[] = $ns_data[0]; - } - } - } - } - - if (!empty($search)) { - // go through all folders detecting namespace usage - foreach ($result as $folder) { - foreach ($search as $idx => $prefix) { - if (strpos($folder, $prefix) === 0) { - unset($search[$idx]); - } - } - if (empty($search)) { - break; - } - } - - // get folders in hidden namespaces and add to the result - foreach ($search as $prefix) { - if ($type == 'ext-subscribed') { - $list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED')); - } - else if ($type == 'subscribed') { - $list = $this->conn->listSubscribed('', $prefix . '*'); - } - else { - $list = $this->conn->listMailboxes('', $prefix . '*'); - } - - if (!empty($list)) { - $result = array_merge($result, $list); - } - } - } - } - - - /** - * Filter the given list of folders according to access rights - */ - protected function filter_rights($a_folders, $rights) - { - $regex = '/('.$rights.')/'; - foreach ($a_folders as $idx => $folder) { - $myrights = join('', (array)$this->my_rights($folder)); - if ($myrights !== null && !preg_match($regex, $myrights)) { - unset($a_folders[$idx]); - } - } - - return $a_folders; - } - - - /** - * Get mailbox quota information - * added by Nuny - * - * @return mixed Quota info or False if not supported - */ - public function get_quota() - { - if ($this->get_capability('QUOTA') && $this->check_connection()) { - return $this->conn->getQuota(); - } - - return false; - } - - - /** - * Get folder size (size of all messages in a folder) - * - * @param string $folder Folder name - * - * @return int Folder size in bytes, False on error - */ - public function folder_size($folder) - { - if (!$this->check_connection()) { - return 0; - } - - // @TODO: could we try to use QUOTA here? - $result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false); - - if (is_array($result)) { - $result = array_sum($result); - } - - return $result; - } - - - /** - * Subscribe to a specific folder(s) - * - * @param array $folders Folder name(s) - * - * @return boolean True on success - */ - public function subscribe($folders) - { - // let this common function do the main work - return $this->change_subscription($folders, 'subscribe'); - } - - - /** - * Unsubscribe folder(s) - * - * @param array $a_mboxes Folder name(s) - * - * @return boolean True on success - */ - public function unsubscribe($folders) - { - // let this common function do the main work - return $this->change_subscription($folders, 'unsubscribe'); - } - - - /** - * Create a new folder on the server and register it in local cache - * - * @param string $folder New folder name - * @param boolean $subscribe True if the new folder should be subscribed - * - * @return boolean True on success - */ - public function create_folder($folder, $subscribe=false) - { - if (!$this->check_connection()) { - return false; - } - - $result = $this->conn->createFolder($folder); - - // try to subscribe it - if ($result) { - // clear cache - $this->clear_cache('mailboxes', true); - - if ($subscribe) { - $this->subscribe($folder); - } - } - - return $result; - } - - - /** - * Set a new name to an existing folder - * - * @param string $folder Folder to rename - * @param string $new_name New folder name - * - * @return boolean True on success - */ - public function rename_folder($folder, $new_name) - { - if (!strlen($new_name)) { - return false; - } - - if (!$this->check_connection()) { - return false; - } - - $delm = $this->get_hierarchy_delimiter(); - - // get list of subscribed folders - if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) { - $a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*'); - $subscribed = $this->folder_exists($folder, true); - } - else { - $a_subscribed = $this->list_folders_subscribed(); - $subscribed = in_array($folder, $a_subscribed); - } - - $result = $this->conn->renameFolder($folder, $new_name); - - if ($result) { - // unsubscribe the old folder, subscribe the new one - if ($subscribed) { - $this->conn->unsubscribe($folder); - $this->conn->subscribe($new_name); - } - - // check if folder children are subscribed - foreach ($a_subscribed as $c_subscribed) { - if (strpos($c_subscribed, $folder.$delm) === 0) { - $this->conn->unsubscribe($c_subscribed); - $this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/', - $new_name, $c_subscribed)); - - // clear cache - $this->clear_message_cache($c_subscribed); - } - } - - // clear cache - $this->clear_message_cache($folder); - $this->clear_cache('mailboxes', true); - } - - return $result; - } - - - /** - * Remove folder from server - * - * @param string $folder Folder name - * - * @return boolean True on success - */ - function delete_folder($folder) - { - $delm = $this->get_hierarchy_delimiter(); - - if (!$this->check_connection()) { - return false; - } - - // get list of folders - if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) { - $sub_mboxes = $this->list_folders('', $folder . $delm . '*'); - } - else { - $sub_mboxes = $this->list_folders(); - } - - // send delete command to server - $result = $this->conn->deleteFolder($folder); - - if ($result) { - // unsubscribe folder - $this->conn->unsubscribe($folder); - - foreach ($sub_mboxes as $c_mbox) { - if (strpos($c_mbox, $folder.$delm) === 0) { - $this->conn->unsubscribe($c_mbox); - if ($this->conn->deleteFolder($c_mbox)) { - $this->clear_message_cache($c_mbox); - } - } - } - - // clear folder-related cache - $this->clear_message_cache($folder); - $this->clear_cache('mailboxes', true); - } - - return $result; - } - - - /** - * Create all folders specified as default - */ - public function create_default_folders() - { - // create default folders if they do not exist - foreach ($this->default_folders as $folder) { - if (!$this->folder_exists($folder)) { - $this->create_folder($folder, true); - } - else if (!$this->folder_exists($folder, true)) { - $this->subscribe($folder); - } - } - } - - - /** - * Checks if folder exists and is subscribed - * - * @param string $folder Folder name - * @param boolean $subscription Enable subscription checking - * - * @return boolean TRUE or FALSE - */ - public function folder_exists($folder, $subscription=false) - { - if ($folder == 'INBOX') { - return true; - } - - $key = $subscription ? 'subscribed' : 'existing'; - - if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) { - return true; - } - - if (!$this->check_connection()) { - return false; - } - - if ($subscription) { - $a_folders = $this->conn->listSubscribed('', $folder); - } - else { - $a_folders = $this->conn->listMailboxes('', $folder); - } - - if (is_array($a_folders) && in_array($folder, $a_folders)) { - $this->icache[$key][] = $folder; - return true; - } - - return false; - } - - - /** - * Returns the namespace where the folder is in - * - * @param string $folder Folder name - * - * @return string One of 'personal', 'other' or 'shared' - */ - public function folder_namespace($folder) - { - if ($folder == 'INBOX') { - return 'personal'; - } - - foreach ($this->namespace as $type => $namespace) { - if (is_array($namespace)) { - foreach ($namespace as $ns) { - if ($len = strlen($ns[0])) { - if (($len > 1 && $folder == substr($ns[0], 0, -1)) - || strpos($folder, $ns[0]) === 0 - ) { - return $type; - } - } - } - } - } - - return 'personal'; - } - - - /** - * Modify folder name according to namespace. - * For output it removes prefix of the personal namespace if it's possible. - * For input it adds the prefix. Use it before creating a folder in root - * of the folders tree. - * - * @param string $folder Folder name - * @param string $mode Mode name (out/in) - * - * @return string Folder name - */ - public function mod_folder($folder, $mode = 'out') - { - if (!strlen($folder)) { - return $folder; - } - - $prefix = $this->namespace['prefix']; // see set_env() - $prefix_len = strlen($prefix); - - if (!$prefix_len) { - return $folder; - } - - // remove prefix for output - if ($mode == 'out') { - if (substr($folder, 0, $prefix_len) === $prefix) { - return substr($folder, $prefix_len); - } - } - // add prefix for input (e.g. folder creation) - else { - return $prefix . $folder; - } - - return $folder; - } - - - /** - * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors - * - * @param string $folder Folder name - * @param bool $force Set to True if attributes should be refreshed - * - * @return array Options list - */ - public function folder_attributes($folder, $force=false) - { - // get attributes directly from LIST command - if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) { - $opts = $this->conn->data['LIST'][$folder]; - } - // get cached folder attributes - else if (!$force) { - $opts = $this->get_cache('mailboxes.attributes'); - $opts = $opts[$folder]; - } - - if (!is_array($opts)) { - if (!$this->check_connection()) { - return array(); - } - - $this->conn->listMailboxes('', $folder); - $opts = $this->conn->data['LIST'][$folder]; - } - - return is_array($opts) ? $opts : array(); - } - - - /** - * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT, - * PERMANENTFLAGS, UIDNEXT, UNSEEN - * - * @param string $folder Folder name - * - * @return array Data - */ - public function folder_data($folder) - { - if (!strlen($folder)) { - $folder = $this->folder !== null ? $this->folder : 'INBOX'; - } - - if ($this->conn->selected != $folder) { - if (!$this->check_connection()) { - return array(); - } - - if ($this->conn->select($folder)) { - $this->folder = $folder; - } - else { - return null; - } - } - - $data = $this->conn->data; - - // add (E)SEARCH result for ALL UNDELETED query - if (!empty($this->icache['undeleted_idx']) - && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder - ) { - $data['UNDELETED'] = $this->icache['undeleted_idx']; - } - - return $data; - } - - - /** - * Returns extended information about the folder - * - * @param string $folder Folder name - * - * @return array Data - */ - public function folder_info($folder) - { - if ($this->icache['options'] && $this->icache['options']['name'] == $folder) { - return $this->icache['options']; - } - - // get cached metadata - $cache_key = 'mailboxes.folder-info.' . $folder; - $cached = $this->get_cache($cache_key); - - if (is_array($cached)) { - return $cached; - } - - $acl = $this->get_capability('ACL'); - $namespace = $this->get_namespace(); - $options = array(); - - // check if the folder is a namespace prefix - if (!empty($namespace)) { - $mbox = $folder . $this->delimiter; - foreach ($namespace as $ns) { - if (!empty($ns)) { - foreach ($ns as $item) { - if ($item[0] === $mbox) { - $options['is_root'] = true; - break 2; - } - } - } - } - } - // check if the folder is other user virtual-root - if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) { - $parts = explode($this->delimiter, $folder); - if (count($parts) == 2) { - $mbox = $parts[0] . $this->delimiter; - foreach ($namespace['other'] as $item) { - if ($item[0] === $mbox) { - $options['is_root'] = true; - break; - } - } - } - } - - $options['name'] = $folder; - $options['attributes'] = $this->folder_attributes($folder, true); - $options['namespace'] = $this->folder_namespace($folder); - $options['special'] = in_array($folder, $this->default_folders); - - // Set 'noselect' flag - if (is_array($options['attributes'])) { - foreach ($options['attributes'] as $attrib) { - $attrib = strtolower($attrib); - if ($attrib == '\noselect' || $attrib == '\nonexistent') { - $options['noselect'] = true; - } - } - } - else { - $options['noselect'] = true; - } - - // Get folder rights (MYRIGHTS) - if ($acl && ($rights = $this->my_rights($folder))) { - $options['rights'] = $rights; - } - - // Set 'norename' flag - if (!empty($options['rights'])) { - $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']); - - if (!$options['noselect']) { - $options['noselect'] = !in_array('r', $options['rights']); - } - } - else { - $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal'; - } - - // update caches - $this->icache['options'] = $options; - $this->update_cache($cache_key, $options); - - return $options; - } - - - /** - * Synchronizes messages cache. - * - * @param string $folder Folder name - */ - public function folder_sync($folder) - { - if ($mcache = $this->get_mcache_engine()) { - $mcache->synchronize($folder); - } - } - - - /** - * Get message header names for rcube_imap_generic::fetchHeader(s) - * - * @return string Space-separated list of header names - */ - protected function get_fetch_headers() - { - if (!empty($this->options['fetch_headers'])) { - $headers = explode(' ', $this->options['fetch_headers']); - $headers = array_map('strtoupper', $headers); - } - else { - $headers = array(); - } - - if ($this->messages_caching || $this->options['all_headers']) { - $headers = array_merge($headers, $this->all_headers); - } - - return implode(' ', array_unique($headers)); - } - - - /* ----------------------------------------- - * ACL and METADATA/ANNOTATEMORE methods - * ----------------------------------------*/ - - /** - * Changes the ACL on the specified folder (SETACL) - * - * @param string $folder Folder name - * @param string $user User name - * @param string $acl ACL string - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - public function set_acl($folder, $user, $acl) - { - if (!$this->get_capability('ACL')) { - return false; - } - - if (!$this->check_connection()) { - return false; - } - - $this->clear_cache('mailboxes.folder-info.' . $folder); - - return $this->conn->setACL($folder, $user, $acl); - } - - - /** - * Removes any <identifier,rights> pair for the - * specified user from the ACL for the specified - * folder (DELETEACL) - * - * @param string $folder Folder name - * @param string $user User name - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - public function delete_acl($folder, $user) - { - if (!$this->get_capability('ACL')) { - return false; - } - - if (!$this->check_connection()) { - return false; - } - - return $this->conn->deleteACL($folder, $user); - } - - - /** - * Returns the access control list for folder (GETACL) - * - * @param string $folder Folder name - * - * @return array User-rights array on success, NULL on error - * @since 0.5-beta - */ - public function get_acl($folder) - { - if (!$this->get_capability('ACL')) { - return null; - } - - if (!$this->check_connection()) { - return null; - } - - return $this->conn->getACL($folder); - } - - - /** - * Returns information about what rights can be granted to the - * user (identifier) in the ACL for the folder (LISTRIGHTS) - * - * @param string $folder Folder name - * @param string $user User name - * - * @return array List of user rights - * @since 0.5-beta - */ - public function list_rights($folder, $user) - { - if (!$this->get_capability('ACL')) { - return null; - } - - if (!$this->check_connection()) { - return null; - } - - return $this->conn->listRights($folder, $user); - } - - - /** - * Returns the set of rights that the current user has to - * folder (MYRIGHTS) - * - * @param string $folder Folder name - * - * @return array MYRIGHTS response on success, NULL on error - * @since 0.5-beta - */ - public function my_rights($folder) - { - if (!$this->get_capability('ACL')) { - return null; - } - - if (!$this->check_connection()) { - return null; - } - - return $this->conn->myRights($folder); - } - - - /** - * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION) - * - * @param string $folder Folder name (empty for server metadata) - * @param array $entries Entry-value array (use NULL value as NIL) - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - public function set_metadata($folder, $entries) - { - if (!$this->check_connection()) { - return false; - } - - $this->clear_cache('mailboxes.metadata.', true); - - if ($this->get_capability('METADATA') || - (!strlen($folder) && $this->get_capability('METADATA-SERVER')) - ) { - return $this->conn->setMetadata($folder, $entries); - } - else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { - foreach ((array)$entries as $entry => $value) { - list($ent, $attr) = $this->md2annotate($entry); - $entries[$entry] = array($ent, $attr, $value); - } - return $this->conn->setAnnotation($folder, $entries); - } - - return false; - } - - - /** - * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION) - * - * @param string $folder Folder name (empty for server metadata) - * @param array $entries Entry names array - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - public function delete_metadata($folder, $entries) - { - if (!$this->check_connection()) { - return false; - } - - $this->clear_cache('mailboxes.metadata.', true); - - if ($this->get_capability('METADATA') || - (!strlen($folder) && $this->get_capability('METADATA-SERVER')) - ) { - return $this->conn->deleteMetadata($folder, $entries); - } - else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { - foreach ((array)$entries as $idx => $entry) { - list($ent, $attr) = $this->md2annotate($entry); - $entries[$idx] = array($ent, $attr, NULL); - } - return $this->conn->setAnnotation($folder, $entries); - } - - return false; - } - - - /** - * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION) - * - * @param string $folder Folder name (empty for server metadata) - * @param array $entries Entries - * @param array $options Command options (with MAXSIZE and DEPTH keys) - * - * @return array Metadata entry-value hash array on success, NULL on error - * @since 0.5-beta - */ - public function get_metadata($folder, $entries, $options=array()) - { - $entries = (array)$entries; - - // create cache key - // @TODO: this is the simplest solution, but we do the same with folders list - // maybe we should store data per-entry and merge on request - sort($options); - sort($entries); - $cache_key = 'mailboxes.metadata.' . $folder; - $cache_key .= '.' . md5(serialize($options).serialize($entries)); - - // get cached data - $cached_data = $this->get_cache($cache_key); - - if (is_array($cached_data)) { - return $cached_data; - } - - if (!$this->check_connection()) { - return null; - } - - if ($this->get_capability('METADATA') || - (!strlen($folder) && $this->get_capability('METADATA-SERVER')) - ) { - $res = $this->conn->getMetadata($folder, $entries, $options); - } - else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) { - $queries = array(); - $res = array(); - - // Convert entry names - foreach ($entries as $entry) { - list($ent, $attr) = $this->md2annotate($entry); - $queries[$attr][] = $ent; - } - - // @TODO: Honor MAXSIZE and DEPTH options - foreach ($queries as $attrib => $entry) { - if ($result = $this->conn->getAnnotation($folder, $entry, $attrib)) { - $res = array_merge_recursive($res, $result); - } - } - } - - if (isset($res)) { - $this->update_cache($cache_key, $res); - return $res; - } - - return null; - } - - - /** - * Converts the METADATA extension entry name into the correct - * entry-attrib names for older ANNOTATEMORE version. - * - * @param string $entry Entry name - * - * @return array Entry-attribute list, NULL if not supported (?) - */ - protected function md2annotate($entry) - { - if (substr($entry, 0, 7) == '/shared') { - return array(substr($entry, 7), 'value.shared'); - } - else if (substr($entry, 0, 8) == '/private') { - return array(substr($entry, 8), 'value.priv'); - } - - // @TODO: log error - return null; - } - - - /* -------------------------------- - * internal caching methods - * --------------------------------*/ - - /** - * Enable or disable indexes caching - * - * @param string $type Cache type (@see rcube::get_cache) - */ - public function set_caching($type) - { - if ($type) { - $this->caching = $type; - } - else { - if ($this->cache) { - $this->cache->close(); - } - $this->cache = null; - $this->caching = false; - } - } - - /** - * Getter for IMAP cache object - */ - protected function get_cache_engine() - { - if ($this->caching && !$this->cache) { - $rcube = rcube::get_instance(); - $ttl = $rcube->config->get('message_cache_lifetime', '10d'); - $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl); - } - - return $this->cache; - } - - /** - * Returns cached value - * - * @param string $key Cache key - * - * @return mixed - */ - public function get_cache($key) - { - if ($cache = $this->get_cache_engine()) { - return $cache->get($key); - } - } - - /** - * Update cache - * - * @param string $key Cache key - * @param mixed $data Data - */ - public function update_cache($key, $data) - { - if ($cache = $this->get_cache_engine()) { - $cache->set($key, $data); - } - } - - /** - * Clears the cache. - * - * @param string $key Cache key name or pattern - * @param boolean $prefix_mode Enable it to clear all keys starting - * with prefix specified in $key - */ - public function clear_cache($key = null, $prefix_mode = false) - { - if ($cache = $this->get_cache_engine()) { - $cache->remove($key, $prefix_mode); - } - } - - /** - * Delete outdated cache entries - */ - public function expunge_cache() - { - if ($this->mcache) { - $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d'); - $this->mcache->expunge($ttl); - } - - if ($this->cache) { - $this->cache->expunge(); - } - } - - - /* -------------------------------- - * message caching methods - * --------------------------------*/ - - /** - * Enable or disable messages caching - * - * @param boolean $set Flag - */ - public function set_messages_caching($set) - { - if ($set) { - $this->messages_caching = true; - } - else { - if ($this->mcache) { - $this->mcache->close(); - } - $this->mcache = null; - $this->messages_caching = false; - } - } - - - /** - * Getter for messages cache object - */ - protected function get_mcache_engine() - { - if ($this->messages_caching && !$this->mcache) { - $rcube = rcube::get_instance(); - if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) { - $this->mcache = new rcube_imap_cache( - $dbh, $this, $userid, $this->options['skip_deleted']); - } - } - - return $this->mcache; - } - - - /** - * Clears the messages cache. - * - * @param string $folder Folder name - * @param array $uids Optional message UIDs to remove from cache - */ - protected function clear_message_cache($folder = null, $uids = null) - { - if ($mcache = $this->get_mcache_engine()) { - $mcache->clear($folder, $uids); - } - } - - - /* -------------------------------- - * protected methods - * --------------------------------*/ - - /** - * Validate the given input and save to local properties - * - * @param string $sort_field Sort column - * @param string $sort_order Sort order - */ - protected function set_sort_order($sort_field, $sort_order) - { - if ($sort_field != null) { - $this->sort_field = asciiwords($sort_field); - } - if ($sort_order != null) { - $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC'; - } - } - - - /** - * Sort folders first by default folders and then in alphabethical order - * - * @param array $a_folders Folders list - */ - protected function sort_folder_list($a_folders) - { - $a_out = $a_defaults = $folders = array(); - - $delimiter = $this->get_hierarchy_delimiter(); - - // find default folders and skip folders starting with '.' - foreach ($a_folders as $i => $folder) { - if ($folder[0] == '.') { - continue; - } - - if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) { - $a_defaults[$p] = $folder; - } - else { - $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP'); - } - } - - // sort folders and place defaults on the top - asort($folders, SORT_LOCALE_STRING); - ksort($a_defaults); - $folders = array_merge($a_defaults, array_keys($folders)); - - // finally we must rebuild the list to move - // subfolders of default folders to their place... - // ...also do this for the rest of folders because - // asort() is not properly sorting case sensitive names - while (list($key, $folder) = each($folders)) { - // set the type of folder name variable (#1485527) - $a_out[] = (string) $folder; - unset($folders[$key]); - $this->rsort($folder, $delimiter, $folders, $a_out); - } - - return $a_out; - } - - - /** - * Recursive method for sorting folders - */ - protected function rsort($folder, $delimiter, &$list, &$out) - { - while (list($key, $name) = each($list)) { - if (strpos($name, $folder.$delimiter) === 0) { - // set the type of folder name variable (#1485527) - $out[] = (string) $name; - unset($list[$key]); - $this->rsort($name, $delimiter, $list, $out); - } - } - reset($list); - } - - - /** - * Find UID of the specified message sequence ID - * - * @param int $id Message (sequence) ID - * @param string $folder Folder name - * - * @return int Message UID - */ - public function id2uid($id, $folder = null) - { - if (!strlen($folder)) { - $folder = $this->folder; - } - - if ($uid = array_search($id, (array)$this->uid_id_map[$folder])) { - return $uid; - } - - if (!$this->check_connection()) { - return null; - } - - $uid = $this->conn->ID2UID($folder, $id); - - $this->uid_id_map[$folder][$uid] = $id; - - return $uid; - } - - - /** - * Subscribe/unsubscribe a list of folders and update local cache - */ - protected function change_subscription($folders, $mode) - { - $updated = false; - - if (!empty($folders)) { - if (!$this->check_connection()) { - return false; - } - - foreach ((array)$folders as $i => $folder) { - $folders[$i] = $folder; - - if ($mode == 'subscribe') { - $updated = $this->conn->subscribe($folder); - } - else if ($mode == 'unsubscribe') { - $updated = $this->conn->unsubscribe($folder); - } - } - } - - // clear cached folders list(s) - if ($updated) { - $this->clear_cache('mailboxes', true); - } - - return $updated; - } - - - /** - * Increde/decrese messagecount for a specific folder - */ - protected function set_messagecount($folder, $mode, $increment) - { - if (!is_numeric($increment)) { - return false; - } - - $mode = strtoupper($mode); - $a_folder_cache = $this->get_cache('messagecount'); - - if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) { - return false; - } - - // add incremental value to messagecount - $a_folder_cache[$folder][$mode] += $increment; - - // there's something wrong, delete from cache - if ($a_folder_cache[$folder][$mode] < 0) { - unset($a_folder_cache[$folder][$mode]); - } - - // write back to cache - $this->update_cache('messagecount', $a_folder_cache); - - return true; - } - - - /** - * Remove messagecount of a specific folder from cache - */ - protected function clear_messagecount($folder, $mode=null) - { - $a_folder_cache = $this->get_cache('messagecount'); - - if (is_array($a_folder_cache[$folder])) { - if ($mode) { - unset($a_folder_cache[$folder][$mode]); - } - else { - unset($a_folder_cache[$folder]); - } - $this->update_cache('messagecount', $a_folder_cache); - } - } - - - /** - * Converts date string/object into IMAP date/time format - */ - protected function date_format($date) - { - if (empty($date)) { - return null; - } - - if (!is_object($date) || !is_a($date, 'DateTime')) { - try { - $timestamp = rcube_utils::strtotime($date); - $date = new DateTime("@".$timestamp); - } - catch (Exception $e) { - return null; - } - } - - return $date->format('d-M-Y H:i:s O'); - } - - - /** - * This is our own debug handler for the IMAP connection - * @access public - */ - public function debug_handler(&$imap, $message) - { - rcube::write_log('imap', $message); - } - - - /** - * Deprecated methods (to be removed) - */ - - public function decode_address_list($input, $max = null, $decode = true, $fallback = null) - { - return rcube_mime::decode_address_list($input, $max, $decode, $fallback); - } - - public function decode_header($input, $fallback = null) - { - return rcube_mime::decode_mime_string((string)$input, $fallback); - } - - public static function decode_mime_string($input, $fallback = null) - { - return rcube_mime::decode_mime_string($input, $fallback); - } - - public function mime_decode($input, $encoding = '7bit') - { - return rcube_mime::decode($input, $encoding); - } - - public static function explode_header_string($separator, $str, $remove_comments = false) - { - return rcube_mime::explode_header_string($separator, $str, $remove_comments); - } - - public function select_mailbox($mailbox) - { - // do nothing - } - - public function set_mailbox($folder) - { - $this->set_folder($folder); - } - - public function get_mailbox_name() - { - return $this->get_folder(); - } - - public function list_headers($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0) - { - return $this->list_messages($folder, $page, $sort_field, $sort_order, $slice); - } - - public function get_headers($uid, $folder = null, $force = false) - { - return $this->get_message_headers($uid, $folder, $force); - } - - public function mailbox_status($folder = null) - { - return $this->folder_status($folder); - } - - public function message_index($folder = '', $sort_field = NULL, $sort_order = NULL) - { - return $this->index($folder, $sort_field, $sort_order); - } - - public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true) - { - return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache); - } - - public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) - { - return $this->list_folders_subscribed($root, $name, $filter, $rights, $skip_sort); - } - - public function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false) - { - return $this->list_folders($root, $name, $filter, $rights, $skip_sort); - } - - public function get_mailbox_size($folder) - { - return $this->folder_size($folder); - } - - public function create_mailbox($folder, $subscribe=false) - { - return $this->create_folder($folder, $subscribe); - } - - public function rename_mailbox($folder, $new_name) - { - return $this->rename_folder($folder, $new_name); - } - - function delete_mailbox($folder) - { - return $this->delete_folder($folder); - } - - function clear_mailbox($folder = null) - { - return $this->clear_folder($folder); - } - - public function mailbox_exists($folder, $subscription=false) - { - return $this->folder_exists($folder, $subscription); - } - - public function mailbox_namespace($folder) - { - return $this->folder_namespace($folder); - } - - public function mod_mailbox($folder, $mode = 'out') - { - return $this->mod_folder($folder, $mode); - } - - public function mailbox_attributes($folder, $force=false) - { - return $this->folder_attributes($folder, $force); - } - - public function mailbox_data($folder) - { - return $this->folder_data($folder); - } - - public function mailbox_info($folder) - { - return $this->folder_info($folder); - } - - public function mailbox_sync($folder) - { - return $this->folder_sync($folder); - } - - public function expunge($folder='', $clear_cache=true) - { - return $this->expunge_folder($folder, $clear_cache); - } - -} diff --git a/program/include/rcube_imap_cache.php b/program/include/rcube_imap_cache.php deleted file mode 100644 index f36ace0eb..000000000 --- a/program/include/rcube_imap_cache.php +++ /dev/null @@ -1,1160 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_imap_cache.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Caching of IMAP folder contents (messages and index) | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Interface class for accessing Roundcube messages cache - * - * @package Cache - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - * @version 1.0 - */ -class rcube_imap_cache -{ - /** - * Instance of rcube_imap - * - * @var rcube_imap - */ - private $imap; - - /** - * Instance of rcube_db - * - * @var rcube_db - */ - private $db; - - /** - * User ID - * - * @var int - */ - private $userid; - - /** - * Internal (in-memory) cache - * - * @var array - */ - private $icache = array(); - - private $skip_deleted = false; - - /** - * List of known flags. Thanks to this we can handle flag changes - * with good performance. Bad thing is we need to know used flags. - */ - public $flags = array( - 1 => 'SEEN', // RFC3501 - 2 => 'DELETED', // RFC3501 - 4 => 'ANSWERED', // RFC3501 - 8 => 'FLAGGED', // RFC3501 - 16 => 'DRAFT', // RFC3501 - 32 => 'MDNSENT', // RFC3503 - 64 => 'FORWARDED', // RFC5550 - 128 => 'SUBMITPENDING', // RFC5550 - 256 => 'SUBMITTED', // RFC5550 - 512 => 'JUNK', - 1024 => 'NONJUNK', - 2048 => 'LABEL1', - 4096 => 'LABEL2', - 8192 => 'LABEL3', - 16384 => 'LABEL4', - 32768 => 'LABEL5', - ); - - - /** - * Object constructor. - */ - function __construct($db, $imap, $userid, $skip_deleted) - { - $this->db = $db; - $this->imap = $imap; - $this->userid = $userid; - $this->skip_deleted = $skip_deleted; - } - - - /** - * Cleanup actions (on shutdown). - */ - public function close() - { - $this->save_icache(); - $this->icache = null; - } - - - /** - * Return (sorted) messages index (UIDs). - * If index doesn't exist or is invalid, will be updated. - * - * @param string $mailbox Folder name - * @param string $sort_field Sorting column - * @param string $sort_order Sorting order (ASC|DESC) - * @param bool $exiting Skip index initialization if it doesn't exist in DB - * - * @return array Messages index - */ - function get_index($mailbox, $sort_field = null, $sort_order = null, $existing = false) - { - if (empty($this->icache[$mailbox])) { - $this->icache[$mailbox] = array(); - } - - $sort_order = strtoupper($sort_order) == 'ASC' ? 'ASC' : 'DESC'; - - // Seek in internal cache - if (array_key_exists('index', $this->icache[$mailbox])) { - // The index was fetched from database already, but not validated yet - if (!array_key_exists('object', $this->icache[$mailbox]['index'])) { - $index = $this->icache[$mailbox]['index']; - } - // We've got a valid index - else if ($sort_field == 'ANY' || $this->icache[$mailbox]['index']['sort_field'] == $sort_field) { - $result = $this->icache[$mailbox]['index']['object']; - if ($result->get_parameters('ORDER') != $sort_order) { - $result->revert(); - } - return $result; - } - } - - // Get index from DB (if DB wasn't already queried) - if (empty($index) && empty($this->icache[$mailbox]['index_queried'])) { - $index = $this->get_index_row($mailbox); - - // set the flag that DB was already queried for index - // this way we'll be able to skip one SELECT, when - // get_index() is called more than once - $this->icache[$mailbox]['index_queried'] = true; - } - - $data = null; - - // @TODO: Think about skipping validation checks. - // If we could check only every 10 minutes, we would be able to skip - // expensive checks, mailbox selection or even IMAP connection, this would require - // additional logic to force cache invalidation in some cases - // and many rcube_imap changes to connect when needed - - // Entry exists, check cache status - if (!empty($index)) { - $exists = true; - - if ($sort_field == 'ANY') { - $sort_field = $index['sort_field']; - } - - if ($sort_field != $index['sort_field']) { - $is_valid = false; - } - else { - $is_valid = $this->validate($mailbox, $index, $exists); - } - - if ($is_valid) { - $data = $index['object']; - // revert the order if needed - if ($data->get_parameters('ORDER') != $sort_order) { - $data->revert(); - } - } - } - else { - if ($existing) { - return null; - } - else if ($sort_field == 'ANY') { - $sort_field = ''; - } - - // Got it in internal cache, so the row already exist - $exists = array_key_exists('index', $this->icache[$mailbox]); - } - - // Index not found, not valid or sort field changed, get index from IMAP server - if ($data === null) { - // Get mailbox data (UIDVALIDITY, counters, etc.) for status check - $mbox_data = $this->imap->folder_data($mailbox); - $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data); - - // insert/update - $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists, $index['modseq']); - } - - $this->icache[$mailbox]['index'] = array( - 'object' => $data, - 'sort_field' => $sort_field, - 'modseq' => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ'] - ); - - return $data; - } - - - /** - * Return messages thread. - * If threaded index doesn't exist or is invalid, will be updated. - * - * @param string $mailbox Folder name - * @param string $sort_field Sorting column - * @param string $sort_order Sorting order (ASC|DESC) - * - * @return array Messages threaded index - */ - function get_thread($mailbox) - { - if (empty($this->icache[$mailbox])) { - $this->icache[$mailbox] = array(); - } - - // Seek in internal cache - if (array_key_exists('thread', $this->icache[$mailbox])) { - return $this->icache[$mailbox]['thread']['object']; - } - - // Get thread from DB (if DB wasn't already queried) - if (empty($this->icache[$mailbox]['thread_queried'])) { - $index = $this->get_thread_row($mailbox); - - // set the flag that DB was already queried for thread - // this way we'll be able to skip one SELECT, when - // get_thread() is called more than once or after clear() - $this->icache[$mailbox]['thread_queried'] = true; - } - - // Entry exist, check cache status - if (!empty($index)) { - $exists = true; - $is_valid = $this->validate($mailbox, $index, $exists); - - if (!$is_valid) { - $index = null; - } - } - - // Index not found or not valid, get index from IMAP server - if ($index === null) { - // Get mailbox data (UIDVALIDITY, counters, etc.) for status check - $mbox_data = $this->imap->folder_data($mailbox); - - if ($mbox_data['EXISTS']) { - // get all threads (default sort order) - $threads = $this->imap->fetch_threads($mailbox, true); - } - else { - $threads = new rcube_result_thread($mailbox, '* THREAD'); - } - - $index['object'] = $threads; - - // insert/update - $this->add_thread_row($mailbox, $threads, $mbox_data, $exists); - } - - $this->icache[$mailbox]['thread'] = $index; - - return $index['object']; - } - - - /** - * Returns list of messages (headers). See rcube_imap::fetch_headers(). - * - * @param string $mailbox Folder name - * @param array $msgs Message UIDs - * - * @return array The list of messages (rcube_message_header) indexed by UID - */ - function get_messages($mailbox, $msgs = array()) - { - if (empty($msgs)) { - return array(); - } - - // Fetch messages from cache - $sql_result = $this->db->query( - "SELECT uid, data, flags" - ." FROM ".$this->db->table_name('cache_messages') - ." WHERE user_id = ?" - ." AND mailbox = ?" - ." AND uid IN (".$this->db->array2list($msgs, 'integer').")", - $this->userid, $mailbox); - - $msgs = array_flip($msgs); - $result = array(); - - while ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $uid = intval($sql_arr['uid']); - $result[$uid] = $this->build_message($sql_arr); - - if (!empty($result[$uid])) { - // save memory, we don't need message body here (?) - $result[$uid]->body = null; - - unset($msgs[$uid]); - } - } - - // Fetch not found messages from IMAP server - if (!empty($msgs)) { - $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true); - - // Insert to DB and add to result list - if (!empty($messages)) { - foreach ($messages as $msg) { - $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result)); - $result[$msg->uid] = $msg; - } - } - } - - return $result; - } - - - /** - * Returns message data. - * - * @param string $mailbox Folder name - * @param int $uid Message UID - * @param bool $update If message doesn't exists in cache it will be fetched - * from IMAP server - * @param bool $no_cache Enables internal cache usage - * - * @return rcube_message_header Message data - */ - function get_message($mailbox, $uid, $update = true, $cache = true) - { - // Check internal cache - if ($this->icache['__message'] - && $this->icache['__message']['mailbox'] == $mailbox - && $this->icache['__message']['object']->uid == $uid - ) { - return $this->icache['__message']['object']; - } - - $sql_result = $this->db->query( - "SELECT flags, data" - ." FROM ".$this->db->table_name('cache_messages') - ." WHERE user_id = ?" - ." AND mailbox = ?" - ." AND uid = ?", - $this->userid, $mailbox, (int)$uid); - - if ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $message = $this->build_message($sql_arr); - $found = true; - } - - // Get the message from IMAP server - if (empty($message) && $update) { - $message = $this->imap->get_message_headers($uid, $mailbox, true); - // cache will be updated in close(), see below - } - - // Save the message in internal cache, will be written to DB in close() - // Common scenario: user opens unseen message - // - get message (SELECT) - // - set message headers/structure (INSERT or UPDATE) - // - set \Seen flag (UPDATE) - // This way we can skip one UPDATE - if (!empty($message) && $cache) { - // Save current message from internal cache - $this->save_icache(); - - $this->icache['__message'] = array( - 'object' => $message, - 'mailbox' => $mailbox, - 'exists' => $found, - 'md5sum' => md5(serialize($message)), - ); - } - - return $message; - } - - - /** - * Saves the message in cache. - * - * @param string $mailbox Folder name - * @param rcube_message_header $message Message data - * @param bool $force Skips message in-cache existance check - */ - function add_message($mailbox, $message, $force = false) - { - if (!is_object($message) || empty($message->uid)) { - return; - } - - $msg = serialize($this->db->encode(clone $message)); - $flags = 0; - - if (!empty($message->flags)) { - foreach ($this->flags as $idx => $flag) { - if (!empty($message->flags[$flag])) { - $flags += $idx; - } - } - } - unset($msg->flags); - - // update cache record (even if it exists, the update - // here will work as select, assume row exist if affected_rows=0) - if (!$force) { - $res = $this->db->query( - "UPDATE ".$this->db->table_name('cache_messages') - ." SET flags = ?, data = ?, changed = ".$this->db->now() - ." WHERE user_id = ?" - ." AND mailbox = ?" - ." AND uid = ?", - $flags, $msg, $this->userid, $mailbox, (int) $message->uid); - - if ($this->db->affected_rows()) { - return; - } - } - - // insert new record - $this->db->query( - "INSERT INTO ".$this->db->table_name('cache_messages') - ." (user_id, mailbox, uid, flags, changed, data)" - ." VALUES (?, ?, ?, ?, ".$this->db->now().", ?)", - $this->userid, $mailbox, (int) $message->uid, $flags, $msg); - } - - - /** - * Sets the flag for specified message. - * - * @param string $mailbox Folder name - * @param array $uids Message UIDs or null to change flag - * of all messages in a folder - * @param string $flag The name of the flag - * @param bool $enabled Flag state - */ - function change_flag($mailbox, $uids, $flag, $enabled = false) - { - if (empty($uids)) { - return; - } - - $flag = strtoupper($flag); - $idx = (int) array_search($flag, $this->flags); - $uids = (array) $uids; - - if (!$idx) { - return; - } - - // Internal cache update - if (($message = $this->icache['__message']) - && $message['mailbox'] === $mailbox - && in_array($message['object']->uid, $uids) - ) { - $message['object']->flags[$flag] = $enabled; - - if (count($uids) == 1) { - return; - } - } - - $this->db->query( - "UPDATE ".$this->db->table_name('cache_messages') - ." SET changed = ".$this->db->now() - .", flags = flags ".($enabled ? "+ $idx" : "- $idx") - ." WHERE user_id = ?" - ." AND mailbox = ?" - .($uids !== null ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "") - ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"), - $this->userid, $mailbox); - } - - - /** - * Removes message(s) from cache. - * - * @param string $mailbox Folder name - * @param array $uids Message UIDs, NULL removes all messages - */ - function remove_message($mailbox = null, $uids = null) - { - if (!strlen($mailbox)) { - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache_messages') - ." WHERE user_id = ?", - $this->userid); - } - else { - // Remove the message from internal cache - if (!empty($uids) && ($message = $this->icache['__message']) - && $message['mailbox'] === $mailbox - && in_array($message['object']->uid, (array)$uids) - ) { - $this->icache['__message'] = null; - } - - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache_messages') - ." WHERE user_id = ?" - ." AND mailbox = ?" - .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""), - $this->userid, $mailbox); - } - } - - - /** - * Clears index cache. - * - * @param string $mailbox Folder name - * @param bool $remove Enable to remove the DB row - */ - function remove_index($mailbox = null, $remove = false) - { - // The index should be only removed from database when - // UIDVALIDITY was detected or the mailbox is empty - // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value - if ($remove) { - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache_index') - ." WHERE user_id = ?" - .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""), - $this->userid - ); - } - else { - $this->db->query( - "UPDATE ".$this->db->table_name('cache_index') - ." SET valid = 0" - ." WHERE user_id = ?" - .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""), - $this->userid - ); - } - - if (strlen($mailbox)) { - unset($this->icache[$mailbox]['index']); - // Index removed, set flag to skip SELECT query in get_index() - $this->icache[$mailbox]['index_queried'] = true; - } - else { - $this->icache = array(); - } - } - - - /** - * Clears thread cache. - * - * @param string $mailbox Folder name - */ - function remove_thread($mailbox = null) - { - $this->db->query( - "DELETE FROM ".$this->db->table_name('cache_thread') - ." WHERE user_id = ?" - .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""), - $this->userid - ); - - if (strlen($mailbox)) { - unset($this->icache[$mailbox]['thread']); - // Thread data removed, set flag to skip SELECT query in get_thread() - $this->icache[$mailbox]['thread_queried'] = true; - } - else { - $this->icache = array(); - } - } - - - /** - * Clears the cache. - * - * @param string $mailbox Folder name - * @param array $uids Message UIDs, NULL removes all messages in a folder - */ - function clear($mailbox = null, $uids = null) - { - $this->remove_index($mailbox, true); - $this->remove_thread($mailbox); - $this->remove_message($mailbox, $uids); - } - - - /** - * Delete cache entries older than TTL - * - * @param string $ttl Lifetime of message cache entries - */ - function expunge($ttl) - { - // get expiration timestamp - $ts = get_offset_time($ttl, -1); - - $this->db->query("DELETE FROM ".$this->db->table_name('cache_messages') - ." WHERE changed < " . $this->db->fromunixtime($ts)); - - $this->db->query("DELETE FROM ".$this->db->table_name('cache_index') - ." WHERE changed < " . $this->db->fromunixtime($ts)); - - $this->db->query("DELETE FROM ".$this->db->table_name('cache_thread') - ." WHERE changed < " . $this->db->fromunixtime($ts)); - } - - - /** - * Fetches index data from database - */ - private function get_index_row($mailbox) - { - // Get index from DB - $sql_result = $this->db->query( - "SELECT data, valid" - ." FROM ".$this->db->table_name('cache_index') - ." WHERE user_id = ?" - ." AND mailbox = ?", - $this->userid, $mailbox); - - if ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $data = explode('@', $sql_arr['data']); - $index = @unserialize($data[0]); - unset($data[0]); - - if (empty($index)) { - $index = new rcube_result_index($mailbox); - } - - return array( - 'valid' => $sql_arr['valid'], - 'object' => $index, - 'sort_field' => $data[1], - 'deleted' => $data[2], - 'validity' => $data[3], - 'uidnext' => $data[4], - 'modseq' => $data[5], - ); - } - - return null; - } - - - /** - * Fetches thread data from database - */ - private function get_thread_row($mailbox) - { - // Get thread from DB - $sql_result = $this->db->query( - "SELECT data" - ." FROM ".$this->db->table_name('cache_thread') - ." WHERE user_id = ?" - ." AND mailbox = ?", - $this->userid, $mailbox); - - if ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $data = explode('@', $sql_arr['data']); - $thread = @unserialize($data[0]); - unset($data[0]); - - if (empty($thread)) { - $thread = new rcube_result_thread($mailbox); - } - - return array( - 'object' => $thread, - 'deleted' => $data[1], - 'validity' => $data[2], - 'uidnext' => $data[3], - ); - } - - return null; - } - - - /** - * Saves index data into database - */ - private function add_index_row($mailbox, $sort_field, - $data, $mbox_data = array(), $exists = false, $modseq = null) - { - $data = array( - serialize($data), - $sort_field, - (int) $this->skip_deleted, - (int) $mbox_data['UIDVALIDITY'], - (int) $mbox_data['UIDNEXT'], - $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'], - ); - $data = implode('@', $data); - - if ($exists) { - $sql_result = $this->db->query( - "UPDATE ".$this->db->table_name('cache_index') - ." SET data = ?, valid = 1, changed = ".$this->db->now() - ." WHERE user_id = ?" - ." AND mailbox = ?", - $data, $this->userid, $mailbox); - } - else { - $sql_result = $this->db->query( - "INSERT INTO ".$this->db->table_name('cache_index') - ." (user_id, mailbox, data, valid, changed)" - ." VALUES (?, ?, ?, 1, ".$this->db->now().")", - $this->userid, $mailbox, $data); - } - } - - - /** - * Saves thread data into database - */ - private function add_thread_row($mailbox, $data, $mbox_data = array(), $exists = false) - { - $data = array( - serialize($data), - (int) $this->skip_deleted, - (int) $mbox_data['UIDVALIDITY'], - (int) $mbox_data['UIDNEXT'], - ); - $data = implode('@', $data); - - if ($exists) { - $sql_result = $this->db->query( - "UPDATE ".$this->db->table_name('cache_thread') - ." SET data = ?, changed = ".$this->db->now() - ." WHERE user_id = ?" - ." AND mailbox = ?", - $data, $this->userid, $mailbox); - } - else { - $sql_result = $this->db->query( - "INSERT INTO ".$this->db->table_name('cache_thread') - ." (user_id, mailbox, data, changed)" - ." VALUES (?, ?, ?, ".$this->db->now().")", - $this->userid, $mailbox, $data); - } - } - - - /** - * Checks index/thread validity - */ - private function validate($mailbox, $index, &$exists = true) - { - $object = $index['object']; - $is_thread = is_a($object, 'rcube_result_thread'); - - // sanity check - if (empty($object)) { - return false; - } - - // Get mailbox data (UIDVALIDITY, counters, etc.) for status check - $mbox_data = $this->imap->folder_data($mailbox); - - // @TODO: Think about skipping validation checks. - // If we could check only every 10 minutes, we would be able to skip - // expensive checks, mailbox selection or even IMAP connection, this would require - // additional logic to force cache invalidation in some cases - // and many rcube_imap changes to connect when needed - - // Check UIDVALIDITY - if ($index['validity'] != $mbox_data['UIDVALIDITY']) { - $this->clear($mailbox); - $exists = false; - return false; - } - - // Folder is empty but cache isn't - if (empty($mbox_data['EXISTS'])) { - if (!$object->is_empty()) { - $this->clear($mailbox); - $exists = false; - return false; - } - } - // Folder is not empty but cache is - else if ($object->is_empty()) { - unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']); - return false; - } - - // Validation flag - if (!$is_thread && empty($index['valid'])) { - unset($this->icache[$mailbox]['index']); - return false; - } - - // Index was created with different skip_deleted setting - if ($this->skip_deleted != $index['deleted']) { - return false; - } - - // Check HIGHESTMODSEQ - if (!empty($index['modseq']) && !empty($mbox_data['HIGHESTMODSEQ']) - && $index['modseq'] == $mbox_data['HIGHESTMODSEQ'] - ) { - return true; - } - - // Check UIDNEXT - if ($index['uidnext'] != $mbox_data['UIDNEXT']) { - unset($this->icache[$mailbox][$is_thread ? 'thread' : 'index']); - return false; - } - - // @TODO: find better validity check for threaded index - if ($is_thread) { - // check messages number... - if (!$this->skip_deleted && $mbox_data['EXISTS'] != $object->count_messages()) { - return false; - } - return true; - } - - // The rest of checks, more expensive - if (!empty($this->skip_deleted)) { - // compare counts if available - if (!empty($mbox_data['UNDELETED']) - && $mbox_data['UNDELETED']->count() != $object->count() - ) { - return false; - } - // compare UID sets - if (!empty($mbox_data['UNDELETED'])) { - $uids_new = $mbox_data['UNDELETED']->get(); - $uids_old = $object->get(); - - if (count($uids_new) != count($uids_old)) { - return false; - } - - sort($uids_new, SORT_NUMERIC); - sort($uids_old, SORT_NUMERIC); - - if ($uids_old != $uids_new) - return false; - } - else { - // get all undeleted messages excluding cached UIDs - $ids = $this->imap->search_once($mailbox, 'ALL UNDELETED NOT UID '. - rcube_imap_generic::compressMessageSet($object->get())); - - if (!$ids->is_empty()) { - return false; - } - } - } - else { - // check messages number... - if ($mbox_data['EXISTS'] != $object->count()) { - return false; - } - // ... and max UID - if ($object->max() != $this->imap->id2uid($mbox_data['EXISTS'], $mailbox, true)) { - return false; - } - } - - return true; - } - - - /** - * Synchronizes the mailbox. - * - * @param string $mailbox Folder name - */ - function synchronize($mailbox) - { - // RFC4549: Synchronization Operations for Disconnected IMAP4 Clients - // RFC4551: IMAP Extension for Conditional STORE Operation - // or Quick Flag Changes Resynchronization - // RFC5162: IMAP Extensions for Quick Mailbox Resynchronization - - // @TODO: synchronize with other methods? - $qresync = $this->imap->get_capability('QRESYNC'); - $condstore = $qresync ? true : $this->imap->get_capability('CONDSTORE'); - - if (!$qresync && !$condstore) { - return; - } - - // Get stored index - $index = $this->get_index_row($mailbox); - - // database is empty - if (empty($index)) { - // set the flag that DB was already queried for index - // this way we'll be able to skip one SELECT in get_index() - $this->icache[$mailbox]['index_queried'] = true; - return; - } - - $this->icache[$mailbox]['index'] = $index; - - // no last HIGHESTMODSEQ value - if (empty($index['modseq'])) { - return; - } - - if (!$this->imap->check_connection()) { - return; - } - - // Enable QRESYNC - $res = $this->imap->conn->enable($qresync ? 'QRESYNC' : 'CONDSTORE'); - if ($res === false) { - return; - } - - // Close mailbox if already selected to get most recent data - if ($this->imap->conn->selected == $mailbox) { - $this->imap->conn->close(); - } - - // Get mailbox data (UIDVALIDITY, HIGHESTMODSEQ, counters, etc.) - $mbox_data = $this->imap->folder_data($mailbox); - - if (empty($mbox_data)) { - return; - } - - // Check UIDVALIDITY - if ($index['validity'] != $mbox_data['UIDVALIDITY']) { - $this->clear($mailbox); - return; - } - - // QRESYNC not supported on specified mailbox - if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) { - return; - } - - // Nothing new - if ($mbox_data['HIGHESTMODSEQ'] == $index['modseq']) { - return; - } - - $uids = array(); - $removed = array(); - - // Get known UIDs - $sql_result = $this->db->query( - "SELECT uid" - ." FROM ".$this->db->table_name('cache_messages') - ." WHERE user_id = ?" - ." AND mailbox = ?", - $this->userid, $mailbox); - - while ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $uids[] = $sql_arr['uid']; - } - - // Synchronize messages data - if (!empty($uids)) { - // Get modified flags and vanished messages - // UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED) - $result = $this->imap->conn->fetch($mailbox, - $uids, true, array('FLAGS'), $index['modseq'], $qresync); - - if (!empty($result)) { - foreach ($result as $id => $msg) { - $uid = $msg->uid; - // Remove deleted message - if ($this->skip_deleted && !empty($msg->flags['DELETED'])) { - $removed[] = $uid; - // Invalidate index - $index['valid'] = false; - continue; - } - - $flags = 0; - if (!empty($msg->flags)) { - foreach ($this->flags as $idx => $flag) { - if (!empty($msg->flags[$flag])) { - $flags += $idx; - } - } - } - - $this->db->query( - "UPDATE ".$this->db->table_name('cache_messages') - ." SET flags = ?, changed = ".$this->db->now() - ." WHERE user_id = ?" - ." AND mailbox = ?" - ." AND uid = ?" - ." AND flags <> ?", - $flags, $this->userid, $mailbox, $uid, $flags); - } - } - - // VANISHED found? - if ($qresync) { - $mbox_data = $this->imap->folder_data($mailbox); - - // Removed messages found - $uids = rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']); - if (!empty($uids)) { - $removed = array_merge($removed, $uids); - // Invalidate index - $index['valid'] = false; - } - } - - // remove messages from database - if (!empty($removed)) { - $this->remove_message($mailbox, $removed); - } - } - - // Invalidate thread index (?) - if (!$index['valid']) { - $this->remove_thread($mailbox); - } - - $sort_field = $index['sort_field']; - $sort_order = $index['object']->get_parameters('ORDER'); - $exists = true; - - // Validate index - if (!$this->validate($mailbox, $index, $exists)) { - // Update index - $data = $this->get_index_data($mailbox, $sort_field, $sort_order, $mbox_data); - } - else { - $data = $index['object']; - } - - // update index and/or HIGHESTMODSEQ value - $this->add_index_row($mailbox, $sort_field, $data, $mbox_data, $exists); - - // update internal cache for get_index() - $this->icache[$mailbox]['index']['object'] = $data; - } - - - /** - * Converts cache row into message object. - * - * @param array $sql_arr Message row data - * - * @return rcube_message_header Message object - */ - private function build_message($sql_arr) - { - $message = $this->db->decode(unserialize($sql_arr['data'])); - - if ($message) { - $message->flags = array(); - foreach ($this->flags as $idx => $flag) { - if (($sql_arr['flags'] & $idx) == $idx) { - $message->flags[$flag] = true; - } - } - } - - return $message; - } - - - /** - * Saves message stored in internal cache - */ - private function save_icache() - { - // Save current message from internal cache - if ($message = $this->icache['__message']) { - // clean up some object's data - $object = $this->message_object_prepare($message['object']); - - // calculate current md5 sum - $md5sum = md5(serialize($object)); - - if ($message['md5sum'] != $md5sum) { - $this->add_message($message['mailbox'], $object, !$message['exists']); - } - - $this->icache['__message']['md5sum'] = $md5sum; - } - } - - - /** - * Prepares message object to be stored in database. - */ - private function message_object_prepare($msg) - { - // Remove body too big (>25kB) - if ($msg->body && strlen($msg->body) > 25 * 1024) { - unset($msg->body); - } - - // Fix mimetype which might be broken by some code when message is displayed - // Another solution would be to use object's copy in rcube_message class - // to prevent related issues, however I'm not sure which is better - if ($msg->mimetype) { - list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype); - } - - if (is_array($msg->structure->parts)) { - foreach ($msg->structure->parts as $idx => $part) { - $msg->structure->parts[$idx] = $this->message_object_prepare($part); - } - } - - return $msg; - } - - - /** - * Fetches index data from IMAP server - */ - private function get_index_data($mailbox, $sort_field, $sort_order, $mbox_data = array()) - { - if (empty($mbox_data)) { - $mbox_data = $this->imap->folder_data($mailbox); - } - - if ($mbox_data['EXISTS']) { - // fetch sorted sequence numbers - $index = $this->imap->index_direct($mailbox, $sort_field, $sort_order); - } - else { - $index = new rcube_result_index($mailbox, '* SORT'); - } - - return $index; - } -} - -// for backward compat. -class rcube_mail_header extends rcube_message_header { } diff --git a/program/include/rcube_imap_generic.php b/program/include/rcube_imap_generic.php deleted file mode 100644 index 52bf0e37a..000000000 --- a/program/include/rcube_imap_generic.php +++ /dev/null @@ -1,3701 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_imap_generic.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Provide alternative IMAP library that doesn't rely on the standard | - | C-Client based version. This allows to function regardless | - | of whether or not the PHP build it's running on has IMAP | - | functionality built-in. | - | | - | Based on Iloha IMAP Library. See http://ilohamail.org/ for details | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - | Author: Ryo Chijiiwa <Ryo@IlohaMail.org> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * PHP based wrapper class to connect to an IMAP server - * - * @package Mail - * @author Aleksander Machniak <alec@alec.pl> - */ -class rcube_imap_generic -{ - public $error; - public $errornum; - public $result; - public $resultcode; - public $selected; - public $data = array(); - public $flags = array( - 'SEEN' => '\\Seen', - 'DELETED' => '\\Deleted', - 'ANSWERED' => '\\Answered', - 'DRAFT' => '\\Draft', - 'FLAGGED' => '\\Flagged', - 'FORWARDED' => '$Forwarded', - 'MDNSENT' => '$MDNSent', - '*' => '\\*', - ); - - private $fp; - private $host; - private $logged = false; - private $capability = array(); - private $capability_readed = false; - private $prefs; - private $cmd_tag; - private $cmd_num = 0; - private $resourceid; - private $_debug = false; - private $_debug_handler = false; - - const ERROR_OK = 0; - const ERROR_NO = -1; - const ERROR_BAD = -2; - const ERROR_BYE = -3; - const ERROR_UNKNOWN = -4; - const ERROR_COMMAND = -5; - const ERROR_READONLY = -6; - - const COMMAND_NORESPONSE = 1; - const COMMAND_CAPABILITY = 2; - const COMMAND_LASTLINE = 4; - - /** - * Object constructor - */ - function __construct() - { - } - - /** - * Send simple (one line) command to the connection stream - * - * @param string $string Command string - * @param bool $endln True if CRLF need to be added at the end of command - * - * @param int Number of bytes sent, False on error - */ - function putLine($string, $endln=true) - { - if (!$this->fp) - return false; - - if ($this->_debug) { - $this->debug('C: '. rtrim($string)); - } - - $res = fwrite($this->fp, $string . ($endln ? "\r\n" : '')); - - if ($res === false) { - @fclose($this->fp); - $this->fp = null; - } - - return $res; - } - - /** - * Send command to the connection stream with Command Continuation - * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support - * - * @param string $string Command string - * @param bool $endln True if CRLF need to be added at the end of command - * - * @return int|bool Number of bytes sent, False on error - */ - function putLineC($string, $endln=true) - { - if (!$this->fp) { - return false; - } - - if ($endln) { - $string .= "\r\n"; - } - - $res = 0; - if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) { - for ($i=0, $cnt=count($parts); $i<$cnt; $i++) { - if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) { - // LITERAL+ support - if ($this->prefs['literal+']) { - $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]); - } - - $bytes = $this->putLine($parts[$i].$parts[$i+1], false); - if ($bytes === false) - return false; - $res += $bytes; - - // don't wait if server supports LITERAL+ capability - if (!$this->prefs['literal+']) { - $line = $this->readLine(1000); - // handle error in command - if ($line[0] != '+') - return false; - } - $i++; - } - else { - $bytes = $this->putLine($parts[$i], false); - if ($bytes === false) - return false; - $res += $bytes; - } - } - } - return $res; - } - - /** - * Reads line from the connection stream - * - * @param int $size Buffer size - * - * @return string Line of text response - */ - function readLine($size=1024) - { - $line = ''; - - if (!$size) { - $size = 1024; - } - - do { - if ($this->eof()) { - return $line ? $line : NULL; - } - - $buffer = fgets($this->fp, $size); - - if ($buffer === false) { - $this->closeSocket(); - break; - } - if ($this->_debug) { - $this->debug('S: '. rtrim($buffer)); - } - $line .= $buffer; - } while (substr($buffer, -1) != "\n"); - - return $line; - } - - /** - * Reads more data from the connection stream when provided - * data contain string literal - * - * @param string $line Response text - * @param bool $escape Enables escaping - * - * @return string Line of text response - */ - function multLine($line, $escape = false) - { - $line = rtrim($line); - if (preg_match('/\{([0-9]+)\}$/', $line, $m)) { - $out = ''; - $str = substr($line, 0, -strlen($m[0])); - $bytes = $m[1]; - - while (strlen($out) < $bytes) { - $line = $this->readBytes($bytes); - if ($line === NULL) - break; - $out .= $line; - } - - $line = $str . ($escape ? $this->escape($out) : $out); - } - - return $line; - } - - /** - * Reads specified number of bytes from the connection stream - * - * @param int $bytes Number of bytes to get - * - * @return string Response text - */ - function readBytes($bytes) - { - $data = ''; - $len = 0; - while ($len < $bytes && !$this->eof()) - { - $d = fread($this->fp, $bytes-$len); - if ($this->_debug) { - $this->debug('S: '. $d); - } - $data .= $d; - $data_len = strlen($data); - if ($len == $data_len) { - break; // nothing was read -> exit to avoid apache lockups - } - $len = $data_len; - } - - return $data; - } - - /** - * Reads complete response to the IMAP command - * - * @param array $untagged Will be filled with untagged response lines - * - * @return string Response text - */ - function readReply(&$untagged=null) - { - do { - $line = trim($this->readLine(1024)); - // store untagged response lines - if ($line[0] == '*') - $untagged[] = $line; - } while ($line[0] == '*'); - - if ($untagged) - $untagged = join("\n", $untagged); - - return $line; - } - - /** - * Response parser. - * - * @param string $string Response text - * @param string $err_prefix Error message prefix - * - * @return int Response status - */ - function parseResult($string, $err_prefix='') - { - if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) { - $res = strtoupper($matches[1]); - $str = trim($matches[2]); - - if ($res == 'OK') { - $this->errornum = self::ERROR_OK; - } else if ($res == 'NO') { - $this->errornum = self::ERROR_NO; - } else if ($res == 'BAD') { - $this->errornum = self::ERROR_BAD; - } else if ($res == 'BYE') { - $this->closeSocket(); - $this->errornum = self::ERROR_BYE; - } - - if ($str) { - $str = trim($str); - // get response string and code (RFC5530) - if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) { - $this->resultcode = strtoupper($m[1]); - $str = trim(substr($str, strlen($m[1]) + 2)); - } - else { - $this->resultcode = null; - // parse response for [APPENDUID 1204196876 3456] - if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) { - $this->data['APPENDUID'] = $m[1]; - } - // parse response for [COPYUID 1204196876 3456:3457 123:124] - else if (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) { - $this->data['COPYUID'] = array($m[1], $m[2]); - } - } - $this->result = $str; - - if ($this->errornum != self::ERROR_OK) { - $this->error = $err_prefix ? $err_prefix.$str : $str; - } - } - - return $this->errornum; - } - return self::ERROR_UNKNOWN; - } - - /** - * Checks connection stream state. - * - * @return bool True if connection is closed - */ - private function eof() - { - if (!is_resource($this->fp)) { - return true; - } - - // If a connection opened by fsockopen() wasn't closed - // by the server, feof() will hang. - $start = microtime(true); - - if (feof($this->fp) || - ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout'])) - ) { - $this->closeSocket(); - return true; - } - - return false; - } - - /** - * Closes connection stream. - */ - private function closeSocket() - { - @fclose($this->fp); - $this->fp = null; - } - - /** - * Error code/message setter. - */ - function setError($code, $msg='') - { - $this->errornum = $code; - $this->error = $msg; - } - - /** - * Checks response status. - * Checks if command response line starts with specified prefix (or * BYE/BAD) - * - * @param string $string Response text - * @param string $match Prefix to match with (case-sensitive) - * @param bool $error Enables BYE/BAD checking - * @param bool $nonempty Enables empty response checking - * - * @return bool True any check is true or connection is closed. - */ - function startsWith($string, $match, $error=false, $nonempty=false) - { - if (!$this->fp) { - return true; - } - if (strncmp($string, $match, strlen($match)) == 0) { - return true; - } - if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) { - if (strtoupper($m[1]) == 'BYE') { - $this->closeSocket(); - } - return true; - } - if ($nonempty && !strlen($string)) { - return true; - } - return false; - } - - private function hasCapability($name) - { - if (empty($this->capability) || $name == '') { - return false; - } - - if (in_array($name, $this->capability)) { - return true; - } - else if (strpos($name, '=')) { - return false; - } - - $result = array(); - foreach ($this->capability as $cap) { - $entry = explode('=', $cap); - if ($entry[0] == $name) { - $result[] = $entry[1]; - } - } - - return !empty($result) ? $result : false; - } - - /** - * Capabilities checker - * - * @param string $name Capability name - * - * @return mixed Capability values array for key=value pairs, true/false for others - */ - function getCapability($name) - { - $result = $this->hasCapability($name); - - if (!empty($result)) { - return $result; - } - else if ($this->capability_readed) { - return false; - } - - // get capabilities (only once) because initial - // optional CAPABILITY response may differ - $result = $this->execute('CAPABILITY'); - - if ($result[0] == self::ERROR_OK) { - $this->parseCapability($result[1]); - } - - $this->capability_readed = true; - - return $this->hasCapability($name); - } - - function clearCapability() - { - $this->capability = array(); - $this->capability_readed = false; - } - - /** - * DIGEST-MD5/CRAM-MD5/PLAIN Authentication - * - * @param string $user - * @param string $pass - * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5) - * - * @return resource Connection resourse on success, error code on error - */ - function authenticate($user, $pass, $type='PLAIN') - { - if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') { - if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) { - $this->setError(self::ERROR_BYE, - "The Auth_SASL package is required for DIGEST-MD5 authentication"); - return self::ERROR_BAD; - } - - $this->putLine($this->nextTag() . " AUTHENTICATE $type"); - $line = trim($this->readReply()); - - if ($line[0] == '+') { - $challenge = substr($line, 2); - } - else { - return $this->parseResult($line); - } - - if ($type == 'CRAM-MD5') { - // RFC2195: CRAM-MD5 - $ipad = ''; - $opad = ''; - - // initialize ipad, opad - for ($i=0; $i<64; $i++) { - $ipad .= chr(0x36); - $opad .= chr(0x5C); - } - - // pad $pass so it's 64 bytes - $padLen = 64 - strlen($pass); - for ($i=0; $i<$padLen; $i++) { - $pass .= chr(0); - } - - // generate hash - $hash = md5($this->_xor($pass, $opad) . pack("H*", - md5($this->_xor($pass, $ipad) . base64_decode($challenge)))); - $reply = base64_encode($user . ' ' . $hash); - - // send result - $this->putLine($reply); - } - else { - // RFC2831: DIGEST-MD5 - // proxy authorization - if (!empty($this->prefs['auth_cid'])) { - $authc = $this->prefs['auth_cid']; - $pass = $this->prefs['auth_pw']; - } - else { - $authc = $user; - $user = ''; - } - $auth_sasl = Auth_SASL::factory('digestmd5'); - $reply = base64_encode($auth_sasl->getResponse($authc, $pass, - base64_decode($challenge), $this->host, 'imap', $user)); - - // send result - $this->putLine($reply); - $line = trim($this->readReply()); - - if ($line[0] == '+') { - $challenge = substr($line, 2); - } - else { - return $this->parseResult($line); - } - - // check response - $challenge = base64_decode($challenge); - if (strpos($challenge, 'rspauth=') === false) { - $this->setError(self::ERROR_BAD, - "Unexpected response from server to DIGEST-MD5 response"); - return self::ERROR_BAD; - } - - $this->putLine(''); - } - - $line = $this->readReply(); - $result = $this->parseResult($line); - } - else { // PLAIN - // proxy authorization - if (!empty($this->prefs['auth_cid'])) { - $authc = $this->prefs['auth_cid']; - $pass = $this->prefs['auth_pw']; - } - else { - $authc = $user; - $user = ''; - } - - $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass); - - // RFC 4959 (SASL-IR): save one round trip - if ($this->getCapability('SASL-IR')) { - list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply), - self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY); - } - else { - $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN"); - $line = trim($this->readReply()); - - if ($line[0] != '+') { - return $this->parseResult($line); - } - - // send result, get reply and process it - $this->putLine($reply); - $line = $this->readReply(); - $result = $this->parseResult($line); - } - } - - if ($result == self::ERROR_OK) { - // optional CAPABILITY response - if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { - $this->parseCapability($matches[1], true); - } - return $this->fp; - } - else { - $this->setError($result, "AUTHENTICATE $type: $line"); - } - - return $result; - } - - /** - * LOGIN Authentication - * - * @param string $user - * @param string $pass - * - * @return resource Connection resourse on success, error code on error - */ - function login($user, $password) - { - list($code, $response) = $this->execute('LOGIN', array( - $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY); - - // re-set capabilities list if untagged CAPABILITY response provided - if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) { - $this->parseCapability($matches[1], true); - } - - if ($code == self::ERROR_OK) { - return $this->fp; - } - - return $code; - } - - /** - * Detects hierarchy delimiter - * - * @return string The delimiter - */ - function getHierarchyDelimiter() - { - if ($this->prefs['delimiter']) { - return $this->prefs['delimiter']; - } - - // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8) - list($code, $response) = $this->execute('LIST', - array($this->escape(''), $this->escape(''))); - - if ($code == self::ERROR_OK) { - $args = $this->tokenizeResponse($response, 4); - $delimiter = $args[3]; - - if (strlen($delimiter) > 0) { - return ($this->prefs['delimiter'] = $delimiter); - } - } - - return NULL; - } - - /** - * NAMESPACE handler (RFC 2342) - * - * @return array Namespace data hash (personal, other, shared) - */ - function getNamespace() - { - if (array_key_exists('namespace', $this->prefs)) { - return $this->prefs['namespace']; - } - - if (!$this->getCapability('NAMESPACE')) { - return self::ERROR_BAD; - } - - list($code, $response) = $this->execute('NAMESPACE'); - - if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) { - $data = $this->tokenizeResponse(substr($response, 11)); - } - - if (!is_array($data)) { - return $code; - } - - $this->prefs['namespace'] = array( - 'personal' => $data[0], - 'other' => $data[1], - 'shared' => $data[2], - ); - - return $this->prefs['namespace']; - } - - /** - * Connects to IMAP server and authenticates. - * - * @param string $host Server hostname or IP - * @param string $user User name - * @param string $password Password - * @param array $options Connection and class options - * - * @return bool True on success, False on failure - */ - function connect($host, $user, $password, $options=null) - { - // set options - if (is_array($options)) { - $this->prefs = $options; - } - // set auth method - if (!empty($this->prefs['auth_type'])) { - $auth_method = strtoupper($this->prefs['auth_type']); - } else { - $auth_method = 'CHECK'; - } - - $result = false; - - // initialize connection - $this->error = ''; - $this->errornum = self::ERROR_OK; - $this->selected = null; - $this->user = $user; - $this->host = $host; - $this->logged = false; - - // check input - if (empty($host)) { - $this->setError(self::ERROR_BAD, "Empty host"); - return false; - } - if (empty($user)) { - $this->setError(self::ERROR_NO, "Empty user"); - return false; - } - if (empty($password)) { - $this->setError(self::ERROR_NO, "Empty password"); - return false; - } - - if (!$this->prefs['port']) { - $this->prefs['port'] = 143; - } - // check for SSL - if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') { - $host = $this->prefs['ssl_mode'] . '://' . $host; - } - - if ($this->prefs['timeout'] <= 0) { - $this->prefs['timeout'] = ini_get('default_socket_timeout'); - } - - // Connect - $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); - - if (!$this->fp) { - $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr)); - return false; - } - - if ($this->prefs['timeout'] > 0) - stream_set_timeout($this->fp, $this->prefs['timeout']); - - $line = trim(fgets($this->fp, 8192)); - - if ($this->_debug) { - // set connection identifier for debug output - preg_match('/#([0-9]+)/', (string)$this->fp, $m); - $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4)); - - if ($line) - $this->debug('S: '. $line); - } - - // Connected to wrong port or connection error? - if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { - if ($line) - $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); - else - $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); - - $this->setError(self::ERROR_BAD, $error); - $this->closeConnection(); - return false; - } - - // RFC3501 [7.1] optional CAPABILITY response - if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { - $this->parseCapability($matches[1], true); - } - - // TLS connection - if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { - if (version_compare(PHP_VERSION, '5.1.0', '>=')) { - $res = $this->execute('STARTTLS'); - - if ($res[0] != self::ERROR_OK) { - $this->closeConnection(); - return false; - } - - if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { - $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); - $this->closeConnection(); - return false; - } - - // Now we're secure, capabilities need to be reread - $this->clearCapability(); - } - } - - // Send ID info - if (!empty($this->prefs['ident']) && $this->getCapability('ID')) { - $this->id($this->prefs['ident']); - } - - $auth_methods = array(); - $result = null; - - // check for supported auth methods - if ($auth_method == 'CHECK') { - if ($auth_caps = $this->getCapability('AUTH')) { - $auth_methods = $auth_caps; - } - // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure - $login_disabled = $this->getCapability('LOGINDISABLED'); - if (($key = array_search('LOGIN', $auth_methods)) !== false) { - if ($login_disabled) { - unset($auth_methods[$key]); - } - } - else if (!$login_disabled) { - $auth_methods[] = 'LOGIN'; - } - - // Use best (for security) supported authentication method - foreach (array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN') as $auth_method) { - if (in_array($auth_method, $auth_methods)) { - break; - } - } - } - else { - // Prevent from sending credentials in plain text when connection is not secure - if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) { - $this->setError(self::ERROR_BAD, "Login disabled by IMAP server"); - $this->closeConnection(); - return false; - } - // replace AUTH with CRAM-MD5 for backward compat. - if ($auth_method == 'AUTH') { - $auth_method = 'CRAM-MD5'; - } - } - - // pre-login capabilities can be not complete - $this->capability_readed = false; - - // Authenticate - switch ($auth_method) { - case 'CRAM_MD5': - $auth_method = 'CRAM-MD5'; - case 'CRAM-MD5': - case 'DIGEST-MD5': - case 'PLAIN': - $result = $this->authenticate($user, $password, $auth_method); - break; - case 'LOGIN': - $result = $this->login($user, $password); - break; - default: - $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method"); - } - - // Connected and authenticated - if (is_resource($result)) { - if ($this->prefs['force_caps']) { - $this->clearCapability(); - } - $this->logged = true; - - return true; - } - - $this->closeConnection(); - - return false; - } - - /** - * Checks connection status - * - * @return bool True if connection is active and user is logged in, False otherwise. - */ - function connected() - { - return ($this->fp && $this->logged) ? true : false; - } - - /** - * Closes connection with logout. - */ - function closeConnection() - { - if ($this->putLine($this->nextTag() . ' LOGOUT')) { - $this->readReply(); - } - - $this->closeSocket(); - } - - /** - * Executes SELECT command (if mailbox is already not in selected state) - * - * @param string $mailbox Mailbox name - * @param array $qresync_data QRESYNC data (RFC5162) - * - * @return boolean True on success, false on error - */ - function select($mailbox, $qresync_data = null) - { - if (!strlen($mailbox)) { - return false; - } - - if ($this->selected === $mailbox) { - return true; - } -/* - Temporary commented out because Courier returns \Noselect for INBOX - Requires more investigation - - if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) { - if (in_array('\\Noselect', $opts)) { - return false; - } - } -*/ - $params = array($this->escape($mailbox)); - - // QRESYNC data items - // 0. the last known UIDVALIDITY, - // 1. the last known modification sequence, - // 2. the optional set of known UIDs, and - // 3. an optional parenthesized list of known sequence ranges and their - // corresponding UIDs. - if (!empty($qresync_data)) { - if (!empty($qresync_data[2])) - $qresync_data[2] = self::compressMessageSet($qresync_data[2]); - $params[] = array('QRESYNC', $qresync_data); - } - - list($code, $response) = $this->execute('SELECT', $params); - - if ($code == self::ERROR_OK) { - $response = explode("\r\n", $response); - foreach ($response as $line) { - if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT)$/i', $line, $m)) { - $this->data[strtoupper($m[2])] = (int) $m[1]; - } - else if (preg_match('/^\* OK \[/i', $line, $match)) { - $line = substr($line, 6); - if (preg_match('/^(UIDNEXT|UIDVALIDITY|UNSEEN) ([0-9]+)/i', $line, $match)) { - $this->data[strtoupper($match[1])] = (int) $match[2]; - } - else if (preg_match('/^(HIGHESTMODSEQ) ([0-9]+)/i', $line, $match)) { - $this->data[strtoupper($match[1])] = (string) $match[2]; - } - else if (preg_match('/^(NOMODSEQ)/i', $line, $match)) { - $this->data[strtoupper($match[1])] = true; - } - else if (preg_match('/^PERMANENTFLAGS \(([^\)]+)\)/iU', $line, $match)) { - $this->data['PERMANENTFLAGS'] = explode(' ', $match[1]); - } - } - // QRESYNC FETCH response (RFC5162) - else if (preg_match('/^\* ([0-9+]) FETCH/i', $line, $match)) { - $line = substr($line, strlen($match[0])); - $fetch_data = $this->tokenizeResponse($line, 1); - $data = array('id' => $match[1]); - - for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) { - $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1]; - } - - $this->data['QRESYNC'][$data['uid']] = $data; - } - // QRESYNC VANISHED response (RFC5162) - else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { - $line = substr($line, strlen($match[0])); - $v_data = $this->tokenizeResponse($line, 1); - - $this->data['VANISHED'] = $v_data; - } - } - - $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY'; - - $this->selected = $mailbox; - return true; - } - - return false; - } - - /** - * Executes STATUS command - * - * @param string $mailbox Mailbox name - * @param array $items Additional requested item names. By default - * MESSAGES and UNSEEN are requested. Other defined - * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT - * - * @return array Status item-value hash - * @since 0.5-beta - */ - function status($mailbox, $items=array()) - { - if (!strlen($mailbox)) { - return false; - } - - if (!in_array('MESSAGES', $items)) { - $items[] = 'MESSAGES'; - } - if (!in_array('UNSEEN', $items)) { - $items[] = 'UNSEEN'; - } - - list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox), - '(' . implode(' ', (array) $items) . ')')); - - if ($code == self::ERROR_OK && preg_match('/\* STATUS /i', $response)) { - $result = array(); - $response = substr($response, 9); // remove prefix "* STATUS " - - list($mbox, $items) = $this->tokenizeResponse($response, 2); - - // Fix for #1487859. Some buggy server returns not quoted - // folder name with spaces. Let's try to handle this situation - if (!is_array($items) && ($pos = strpos($response, '(')) !== false) { - $response = substr($response, $pos); - $items = $this->tokenizeResponse($response, 1); - if (!is_array($items)) { - return $result; - } - } - - for ($i=0, $len=count($items); $i<$len; $i += 2) { - $result[$items[$i]] = $items[$i+1]; - } - - $this->data['STATUS:'.$mailbox] = $result; - - return $result; - } - - return false; - } - - /** - * Executes EXPUNGE command - * - * @param string $mailbox Mailbox name - * @param string $messages Message UIDs to expunge - * - * @return boolean True on success, False on error - */ - function expunge($mailbox, $messages=NULL) - { - if (!$this->select($mailbox)) { - return false; - } - - if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE'); - return false; - } - - // Clear internal status cache - unset($this->data['STATUS:'.$mailbox]); - - if ($messages) - $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE); - else - $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE); - - if ($result == self::ERROR_OK) { - $this->selected = null; // state has changed, need to reselect - return true; - } - - return false; - } - - /** - * Executes CLOSE command - * - * @return boolean True on success, False on error - * @since 0.5 - */ - function close() - { - $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE); - - if ($result == self::ERROR_OK) { - $this->selected = null; - return true; - } - - return false; - } - - /** - * Folder subscription (SUBSCRIBE) - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - function subscribe($mailbox) - { - $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Folder unsubscription (UNSUBSCRIBE) - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - function unsubscribe($mailbox) - { - $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Folder creation (CREATE) - * - * @param string $mailbox Mailbox name - * - * @return bool True on success, False on error - */ - function createFolder($mailbox) - { - $result = $this->execute('CREATE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Folder renaming (RENAME) - * - * @param string $mailbox Mailbox name - * - * @return bool True on success, False on error - */ - function renameFolder($from, $to) - { - $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Executes DELETE command - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - function deleteFolder($mailbox) - { - $result = $this->execute('DELETE', array($this->escape($mailbox)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Removes all messages in a folder - * - * @param string $mailbox Mailbox name - * - * @return boolean True on success, False on error - */ - function clearFolder($mailbox) - { - $num_in_trash = $this->countMessages($mailbox); - if ($num_in_trash > 0) { - $res = $this->flag($mailbox, '1:*', 'DELETED'); - } - - if ($res) { - if ($this->selected === $mailbox) - $res = $this->close(); - else - $res = $this->expunge($mailbox); - } - - return $res; - } - - /** - * Returns list of mailboxes - * - * @param string $ref Reference name - * @param string $mailbox Mailbox name - * @param array $status_opts (see self::_listMailboxes) - * @param array $select_opts (see self::_listMailboxes) - * - * @return array List of mailboxes or hash of options if $status_opts argument - * is non-empty. - */ - function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array()) - { - return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts); - } - - /** - * Returns list of subscribed mailboxes - * - * @param string $ref Reference name - * @param string $mailbox Mailbox name - * @param array $status_opts (see self::_listMailboxes) - * - * @return array List of mailboxes or hash of options if $status_opts argument - * is non-empty. - */ - function listSubscribed($ref, $mailbox, $status_opts=array()) - { - return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL); - } - - /** - * IMAP LIST/LSUB command - * - * @param string $ref Reference name - * @param string $mailbox Mailbox name - * @param bool $subscribed Enables returning subscribed mailboxes only - * @param array $status_opts List of STATUS options (RFC5819: LIST-STATUS) - * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN - * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED) - * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE - * - * @return array List of mailboxes or hash of options if $status_ops argument - * is non-empty. - */ - private function _listMailboxes($ref, $mailbox, $subscribed=false, - $status_opts=array(), $select_opts=array()) - { - if (!strlen($mailbox)) { - $mailbox = '*'; - } - - $args = array(); - - if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) { - $select_opts = (array) $select_opts; - - $args[] = '(' . implode(' ', $select_opts) . ')'; - } - - $args[] = $this->escape($ref); - $args[] = $this->escape($mailbox); - - if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) { - $status_opts = (array) $status_opts; - $lstatus = true; - - $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))'; - } - - list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args); - - if ($code == self::ERROR_OK) { - $folders = array(); - $last = 0; - $pos = 0; - $response .= "\r\n"; - - while ($pos = strpos($response, "\r\n", $pos+1)) { - // literal string, not real end-of-command-line - if ($response[$pos-1] == '}') { - continue; - } - - $line = substr($response, $last, $pos - $last); - $last = $pos + 2; - - if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) { - continue; - } - $cmd = strtoupper($m[1]); - $line = substr($line, strlen($m[0])); - - // * LIST (<options>) <delimiter> <mailbox> - if ($cmd == 'LIST' || $cmd == 'LSUB') { - list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3); - - // Add to result array - if (!$lstatus) { - $folders[] = $mailbox; - } - else { - $folders[$mailbox] = array(); - } - - // store LSUB options only if not empty, this way - // we can detect a situation when LIST doesn't return specified folder - if (!empty($opts) || $cmd == 'LIST') { - // Add to options array - if (empty($this->data['LIST'][$mailbox])) - $this->data['LIST'][$mailbox] = $opts; - else if (!empty($opts)) - $this->data['LIST'][$mailbox] = array_unique(array_merge( - $this->data['LIST'][$mailbox], $opts)); - } - } - // * STATUS <mailbox> (<result>) - else if ($cmd == 'STATUS') { - list($mailbox, $status) = $this->tokenizeResponse($line, 2); - - for ($i=0, $len=count($status); $i<$len; $i += 2) { - list($name, $value) = $this->tokenizeResponse($status, 2); - $folders[$mailbox][$name] = $value; - } - } - } - - return $folders; - } - - return false; - } - - /** - * Returns count of all messages in a folder - * - * @param string $mailbox Mailbox name - * - * @return int Number of messages, False on error - */ - function countMessages($mailbox, $refresh = false) - { - if ($refresh) { - $this->selected = null; - } - - if ($this->selected === $mailbox) { - return $this->data['EXISTS']; - } - - // Check internal cache - $cache = $this->data['STATUS:'.$mailbox]; - if (!empty($cache) && isset($cache['MESSAGES'])) { - return (int) $cache['MESSAGES']; - } - - // Try STATUS (should be faster than SELECT) - $counts = $this->status($mailbox); - if (is_array($counts)) { - return (int) $counts['MESSAGES']; - } - - return false; - } - - /** - * Returns count of messages with \Recent flag in a folder - * - * @param string $mailbox Mailbox name - * - * @return int Number of messages, False on error - */ - function countRecent($mailbox) - { - if (!strlen($mailbox)) { - $mailbox = 'INBOX'; - } - - $this->select($mailbox); - - if ($this->selected === $mailbox) { - return $this->data['RECENT']; - } - - return false; - } - - /** - * Returns count of messages without \Seen flag in a specified folder - * - * @param string $mailbox Mailbox name - * - * @return int Number of messages, False on error - */ - function countUnseen($mailbox) - { - // Check internal cache - $cache = $this->data['STATUS:'.$mailbox]; - if (!empty($cache) && isset($cache['UNSEEN'])) { - return (int) $cache['UNSEEN']; - } - - // Try STATUS (should be faster than SELECT+SEARCH) - $counts = $this->status($mailbox); - if (is_array($counts)) { - return (int) $counts['UNSEEN']; - } - - // Invoke SEARCH as a fallback - $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT')); - if (!$index->is_error()) { - return $index->count(); - } - - return false; - } - - /** - * Executes ID command (RFC2971) - * - * @param array $items Client identification information key/value hash - * - * @return array Server identification information key/value hash - * @since 0.6 - */ - function id($items=array()) - { - if (is_array($items) && !empty($items)) { - foreach ($items as $key => $value) { - $args[] = $this->escape($key, true); - $args[] = $this->escape($value, true); - } - } - - list($code, $response) = $this->execute('ID', array( - !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null) - )); - - - if ($code == self::ERROR_OK && preg_match('/\* ID /i', $response)) { - $response = substr($response, 5); // remove prefix "* ID " - $items = $this->tokenizeResponse($response, 1); - $result = null; - - for ($i=0, $len=count($items); $i<$len; $i += 2) { - $result[$items[$i]] = $items[$i+1]; - } - - return $result; - } - - return false; - } - - /** - * Executes ENABLE command (RFC5161) - * - * @param mixed $extension Extension name to enable (or array of names) - * - * @return array|bool List of enabled extensions, False on error - * @since 0.6 - */ - function enable($extension) - { - if (empty($extension)) { - return false; - } - - if (!$this->hasCapability('ENABLE')) { - return false; - } - - if (!is_array($extension)) { - $extension = array($extension); - } - - if (!empty($this->extensions_enabled)) { - // check if all extensions are already enabled - $diff = array_diff($extension, $this->extensions_enabled); - - if (empty($diff)) { - return $extension; - } - - // Make sure the mailbox isn't selected, before enabling extension(s) - if ($this->selected !== null) { - $this->close(); - } - } - - list($code, $response) = $this->execute('ENABLE', $extension); - - if ($code == self::ERROR_OK && preg_match('/\* ENABLED /i', $response)) { - $response = substr($response, 10); // remove prefix "* ENABLED " - $result = (array) $this->tokenizeResponse($response); - - $this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result)); - - return $this->extensions_enabled; - } - - return false; - } - - /** - * Executes SORT command - * - * @param string $mailbox Mailbox name - * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) - * @param string $add Searching criteria - * @param bool $return_uid Enables UID SORT usage - * @param string $encoding Character set - * - * @return rcube_result_index Response data - */ - function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII') - { - $field = strtoupper($field); - if ($field == 'INTERNALDATE') { - $field = 'ARRIVAL'; - } - - $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1, - 'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1); - - if (!$fields[$field]) { - return new rcube_result_index($mailbox); - } - - if (!$this->select($mailbox)) { - return new rcube_result_index($mailbox); - } - - // RFC 5957: SORT=DISPLAY - if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) { - $field = 'DISPLAY' . $field; - } - - // message IDs - if (!empty($add)) - $add = $this->compressMessageSet($add); - - list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT', - array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : ''))); - - if ($code != self::ERROR_OK) { - $response = null; - } - - return new rcube_result_index($mailbox, $response); - } - - /** - * Executes THREAD command - * - * @param string $mailbox Mailbox name - * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS) - * @param string $criteria Searching criteria - * @param bool $return_uid Enables UIDs in result instead of sequence numbers - * @param string $encoding Character set - * - * @return rcube_result_thread Thread data - */ - function thread($mailbox, $algorithm='REFERENCES', $criteria='', $return_uid=false, $encoding='US-ASCII') - { - $old_sel = $this->selected; - - if (!$this->select($mailbox)) { - return new rcube_result_thread($mailbox); - } - - // return empty result when folder is empty and we're just after SELECT - if ($old_sel != $mailbox && !$this->data['EXISTS']) { - return new rcube_result_thread($mailbox); - } - - $encoding = $encoding ? trim($encoding) : 'US-ASCII'; - $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES'; - $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL'; - $data = ''; - - list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD', - array($algorithm, $encoding, $criteria)); - - if ($code != self::ERROR_OK) { - $response = null; - } - - return new rcube_result_thread($mailbox, $response); - } - - /** - * Executes SEARCH command - * - * @param string $mailbox Mailbox name - * @param string $criteria Searching criteria - * @param bool $return_uid Enable UID in result instead of sequence ID - * @param array $items Return items (MIN, MAX, COUNT, ALL) - * - * @return rcube_result_index Result data - */ - function search($mailbox, $criteria, $return_uid=false, $items=array()) - { - $old_sel = $this->selected; - - if (!$this->select($mailbox)) { - return new rcube_result_index($mailbox); - } - - // return empty result when folder is empty and we're just after SELECT - if ($old_sel != $mailbox && !$this->data['EXISTS']) { - return new rcube_result_index($mailbox, '* SEARCH'); - } - - // If ESEARCH is supported always use ALL - // but not when items are specified or using simple id2uid search - if (empty($items) && preg_match('/[^0-9]/', $criteria)) { - $items = array('ALL'); - } - - $esearch = empty($items) ? false : $this->getCapability('ESEARCH'); - $criteria = trim($criteria); - $params = ''; - - // RFC4731: ESEARCH - if (!empty($items) && $esearch) { - $params .= 'RETURN (' . implode(' ', $items) . ')'; - } - - if (!empty($criteria)) { - $modseq = stripos($criteria, 'MODSEQ') !== false; - $params .= ($params ? ' ' : '') . $criteria; - } - else { - $params .= 'ALL'; - } - - list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH', - array($params)); - - if ($code != self::ERROR_OK) { - $response = null; - } - - return new rcube_result_index($mailbox, $response); - } - - /** - * Simulates SORT command by using FETCH and sorting. - * - * @param string $mailbox Mailbox name - * @param string|array $message_set Searching criteria (list of messages to return) - * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO) - * @param bool $skip_deleted Makes that DELETED messages will be skipped - * @param bool $uidfetch Enables UID FETCH usage - * @param bool $return_uid Enables returning UIDs instead of IDs - * - * @return rcube_result_index Response data - */ - function index($mailbox, $message_set, $index_field='', $skip_deleted=true, - $uidfetch=false, $return_uid=false) - { - $msg_index = $this->fetchHeaderIndex($mailbox, $message_set, - $index_field, $skip_deleted, $uidfetch, $return_uid); - - if (!empty($msg_index)) { - asort($msg_index); // ASC - $msg_index = array_keys($msg_index); - $msg_index = '* SEARCH ' . implode(' ', $msg_index); - } - else { - $msg_index = is_array($msg_index) ? '* SEARCH' : null; - } - - return new rcube_result_index($mailbox, $msg_index); - } - - function fetchHeaderIndex($mailbox, $message_set, $index_field='', $skip_deleted=true, - $uidfetch=false, $return_uid=false) - { - if (is_array($message_set)) { - if (!($message_set = $this->compressMessageSet($message_set))) - return false; - } else { - list($from_idx, $to_idx) = explode(':', $message_set); - if (empty($message_set) || - (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)) { - return false; - } - } - - $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field); - - $fields_a['DATE'] = 1; - $fields_a['INTERNALDATE'] = 4; - $fields_a['ARRIVAL'] = 4; - $fields_a['FROM'] = 1; - $fields_a['REPLY-TO'] = 1; - $fields_a['SENDER'] = 1; - $fields_a['TO'] = 1; - $fields_a['CC'] = 1; - $fields_a['SUBJECT'] = 1; - $fields_a['UID'] = 2; - $fields_a['SIZE'] = 2; - $fields_a['SEEN'] = 3; - $fields_a['RECENT'] = 3; - $fields_a['DELETED'] = 3; - - if (!($mode = $fields_a[$index_field])) { - return false; - } - - /* Do "SELECT" command */ - if (!$this->select($mailbox)) { - return false; - } - - // build FETCH command string - $key = $this->nextTag(); - $cmd = $uidfetch ? 'UID FETCH' : 'FETCH'; - $fields = array(); - - if ($return_uid) - $fields[] = 'UID'; - if ($skip_deleted) - $fields[] = 'FLAGS'; - - if ($mode == 1) { - if ($index_field == 'DATE') - $fields[] = 'INTERNALDATE'; - $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]"; - } - else if ($mode == 2) { - if ($index_field == 'SIZE') - $fields[] = 'RFC822.SIZE'; - else if (!$return_uid || $index_field != 'UID') - $fields[] = $index_field; - } - else if ($mode == 3 && !$skip_deleted) - $fields[] = 'FLAGS'; - else if ($mode == 4) - $fields[] = 'INTERNALDATE'; - - $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")"; - - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - return false; - } - - $result = array(); - - do { - $line = rtrim($this->readLine(200)); - $line = $this->multLine($line); - - if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { - $id = $m[1]; - $flags = NULL; - - if ($return_uid) { - if (preg_match('/UID ([0-9]+)/', $line, $matches)) - $id = (int) $matches[1]; - else - continue; - } - if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { - $flags = explode(' ', strtoupper($matches[1])); - if (in_array('\\DELETED', $flags)) { - $deleted[$id] = $id; - continue; - } - } - - if ($mode == 1 && $index_field == 'DATE') { - if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) { - $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]); - $value = trim($value); - $result[$id] = $this->strToTime($value); - } - // non-existent/empty Date: header, use INTERNALDATE - if (empty($result[$id])) { - if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) - $result[$id] = $this->strToTime($matches[1]); - else - $result[$id] = 0; - } - } else if ($mode == 1) { - if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) { - $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]); - $result[$id] = trim($value); - } else { - $result[$id] = ''; - } - } else if ($mode == 2) { - if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) { - $result[$id] = trim($matches[2]); - } else { - $result[$id] = 0; - } - } else if ($mode == 3) { - if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) { - $flags = explode(' ', $matches[1]); - } - $result[$id] = in_array('\\'.$index_field, $flags) ? 1 : 0; - } else if ($mode == 4) { - if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) { - $result[$id] = $this->strToTime($matches[1]); - } else { - $result[$id] = 0; - } - } - } - } while (!$this->startsWith($line, $key, true, true)); - - return $result; - } - - /** - * Returns message sequence identifier - * - * @param string $mailbox Mailbox name - * @param int $uid Message unique identifier (UID) - * - * @return int Message sequence identifier - */ - function UID2ID($mailbox, $uid) - { - if ($uid > 0) { - $index = $this->search($mailbox, "UID $uid"); - - if ($index->count() == 1) { - $arr = $index->get(); - return (int) $arr[0]; - } - } - return null; - } - - /** - * Returns message unique identifier (UID) - * - * @param string $mailbox Mailbox name - * @param int $uid Message sequence identifier - * - * @return int Message unique identifier - */ - function ID2UID($mailbox, $id) - { - if (empty($id) || $id < 0) { - return null; - } - - if (!$this->select($mailbox)) { - return null; - } - - $index = $this->search($mailbox, $id, true); - - if ($index->count() == 1) { - $arr = $index->get(); - return (int) $arr[0]; - } - - return null; - } - - /** - * Sets flag of the message(s) - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UID(s) - * @param string $flag Flag name - * - * @return bool True on success, False on failure - */ - function flag($mailbox, $messages, $flag) { - return $this->modFlag($mailbox, $messages, $flag, '+'); - } - - /** - * Unsets flag of the message(s) - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UID(s) - * @param string $flag Flag name - * - * @return bool True on success, False on failure - */ - function unflag($mailbox, $messages, $flag) { - return $this->modFlag($mailbox, $messages, $flag, '-'); - } - - /** - * Changes flag of the message(s) - * - * @param string $mailbox Mailbox name - * @param string|array $messages Message UID(s) - * @param string $flag Flag name - * @param string $mod Modifier [+|-]. Default: "+". - * - * @return bool True on success, False on failure - */ - private function modFlag($mailbox, $messages, $flag, $mod = '+') - { - if ($mod != '+' && $mod != '-') { - $mod = '+'; - } - - if (!$this->select($mailbox)) { - return false; - } - - if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); - return false; - } - - // Clear internal status cache - if ($flag == 'SEEN') { - unset($this->data['STATUS:'.$mailbox]['UNSEEN']); - } - - $flag = $this->flags[strtoupper($flag)]; - $result = $this->execute('UID STORE', array( - $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Copies message(s) from one folder to another - * - * @param string|array $messages Message UID(s) - * @param string $from Mailbox name - * @param string $to Destination mailbox name - * - * @return bool True on success, False on failure - */ - function copy($messages, $from, $to) - { - // Clear last COPYUID data - unset($this->data['COPYUID']); - - if (!$this->select($from)) { - return false; - } - - // Clear internal status cache - unset($this->data['STATUS:'.$to]); - - $result = $this->execute('UID COPY', array( - $this->compressMessageSet($messages), $this->escape($to)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Moves message(s) from one folder to another. - * Original message(s) will be marked as deleted. - * - * @param string|array $messages Message UID(s) - * @param string $from Mailbox name - * @param string $to Destination mailbox name - * - * @return bool True on success, False on failure - */ - function move($messages, $from, $to) - { - if (!$this->select($from)) { - return false; - } - - if (!$this->data['READ-WRITE']) { - $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE'); - return false; - } - - $r = $this->copy($messages, $from, $to); - - if ($r) { - // Clear internal status cache - unset($this->data['STATUS:'.$from]); - - return $this->flag($from, $messages, 'DELETED'); - } - return $r; - } - - /** - * FETCH command (RFC3501) - * - * @param string $mailbox Mailbox name - * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) - * @param bool $is_uid True if $message_set contains UIDs - * @param array $query_items FETCH command data items - * @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query - * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query - * - * @return array List of rcube_message_header elements, False on error - * @since 0.6 - */ - function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(), - $mod_seq = null, $vanished = false) - { - if (!$this->select($mailbox)) { - return false; - } - - $message_set = $this->compressMessageSet($message_set); - $result = array(); - - $key = $this->nextTag(); - $request = $key . ($is_uid ? ' UID' : '') . " FETCH $message_set "; - $request .= "(" . implode(' ', $query_items) . ")"; - - if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) { - $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")"; - } - - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - return false; - } - - do { - $line = $this->readLine(4096); - - if (!$line) - break; - - // Sample reply line: - // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen) - // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...) - // BODY[HEADER.FIELDS ... - - if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) { - $id = intval($m[1]); - - $result[$id] = new rcube_message_header; - $result[$id]->id = $id; - $result[$id]->subject = ''; - $result[$id]->messageID = 'mid:' . $id; - - $headers = null; - $lines = array(); - $line = substr($line, strlen($m[0]) + 2); - $ln = 0; - - // get complete entry - while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) { - $bytes = $m[1]; - $out = ''; - - while (strlen($out) < $bytes) { - $out = $this->readBytes($bytes); - if ($out === NULL) - break; - $line .= $out; - } - - $str = $this->readLine(4096); - if ($str === false) - break; - - $line .= $str; - } - - // Tokenize response and assign to object properties - while (list($name, $value) = $this->tokenizeResponse($line, 2)) { - if ($name == 'UID') { - $result[$id]->uid = intval($value); - } - else if ($name == 'RFC822.SIZE') { - $result[$id]->size = intval($value); - } - else if ($name == 'RFC822.TEXT') { - $result[$id]->body = $value; - } - else if ($name == 'INTERNALDATE') { - $result[$id]->internaldate = $value; - $result[$id]->date = $value; - $result[$id]->timestamp = $this->StrToTime($value); - } - else if ($name == 'FLAGS') { - if (!empty($value)) { - foreach ((array)$value as $flag) { - $flag = str_replace(array('$', '\\'), '', $flag); - $flag = strtoupper($flag); - - $result[$id]->flags[$flag] = true; - } - } - } - else if ($name == 'MODSEQ') { - $result[$id]->modseq = $value[0]; - } - else if ($name == 'ENVELOPE') { - $result[$id]->envelope = $value; - } - else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) { - if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) { - $value = array($value); - } - $result[$id]->bodystructure = $value; - } - else if ($name == 'RFC822') { - $result[$id]->body = $value; - } - else if ($name == 'BODY') { - $body = $this->tokenizeResponse($line, 1); - if ($value[0] == 'HEADER.FIELDS') - $headers = $body; - else if (!empty($value)) - $result[$id]->bodypart[$value[0]] = $body; - else - $result[$id]->body = $body; - } - } - - // create array with header field:data - if (!empty($headers)) { - $headers = explode("\n", trim($headers)); - foreach ($headers as $hid => $resln) { - if (ord($resln[0]) <= 32) { - $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln); - } else { - $lines[++$ln] = trim($resln); - } - } - - while (list($lines_key, $str) = each($lines)) { - list($field, $string) = explode(':', $str, 2); - - $field = strtolower($field); - $string = preg_replace('/\n[\t\s]*/', ' ', trim($string)); - - switch ($field) { - case 'date'; - $result[$id]->date = $string; - $result[$id]->timestamp = $this->strToTime($string); - break; - case 'from': - $result[$id]->from = $string; - break; - case 'to': - $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string); - break; - case 'subject': - $result[$id]->subject = $string; - break; - case 'reply-to': - $result[$id]->replyto = $string; - break; - case 'cc': - $result[$id]->cc = $string; - break; - case 'bcc': - $result[$id]->bcc = $string; - break; - case 'content-transfer-encoding': - $result[$id]->encoding = $string; - break; - case 'content-type': - $ctype_parts = preg_split('/[; ]+/', $string); - $result[$id]->ctype = strtolower(array_shift($ctype_parts)); - if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) { - $result[$id]->charset = $regs[1]; - } - break; - case 'in-reply-to': - $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string); - break; - case 'references': - $result[$id]->references = $string; - break; - case 'return-receipt-to': - case 'disposition-notification-to': - case 'x-confirm-reading-to': - $result[$id]->mdn_to = $string; - break; - case 'message-id': - $result[$id]->messageID = $string; - break; - case 'x-priority': - if (preg_match('/^(\d+)/', $string, $matches)) { - $result[$id]->priority = intval($matches[1]); - } - break; - default: - if (strlen($field) > 2) { - $result[$id]->others[$field] = $string; - } - break; - } - } - } - } - - // VANISHED response (QRESYNC RFC5162) - // Sample: * VANISHED (EARLIER) 300:310,405,411 - - else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) { - $line = substr($line, strlen($match[0])); - $v_data = $this->tokenizeResponse($line, 1); - - $this->data['VANISHED'] = $v_data; - } - - } while (!$this->startsWith($line, $key, true)); - - return $result; - } - - function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '') - { - $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'); - if ($bodystr) - $query_items[] = 'BODYSTRUCTURE'; - $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' - . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY' - . ($add ? ' ' . trim($add) : '') - . ')]'; - - $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items); - - return $result; - } - - function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='') - { - $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add); - if (is_array($a)) { - return array_shift($a); - } - return false; - } - - function sortHeaders($a, $field, $flag) - { - if (empty($field)) { - $field = 'uid'; - } - else { - $field = strtolower($field); - } - - if ($field == 'date' || $field == 'internaldate') { - $field = 'timestamp'; - } - - if (empty($flag)) { - $flag = 'ASC'; - } else { - $flag = strtoupper($flag); - } - - $c = count($a); - if ($c > 0) { - // Strategy: - // First, we'll create an "index" array. - // Then, we'll use sort() on that array, - // and use that to sort the main array. - - // create "index" array - $index = array(); - reset($a); - while (list($key, $val) = each($a)) { - if ($field == 'timestamp') { - $data = $this->strToTime($val->date); - if (!$data) { - $data = $val->timestamp; - } - } else { - $data = $val->$field; - if (is_string($data)) { - $data = str_replace('"', '', $data); - if ($field == 'subject') { - $data = preg_replace('/^(Re: \s*|Fwd:\s*|Fw:\s*)+/i', '', $data); - } - $data = strtoupper($data); - } - } - $index[$key] = $data; - } - - // sort index - if ($flag == 'ASC') { - asort($index); - } else { - arsort($index); - } - - // form new array based on index - $result = array(); - reset($index); - while (list($key, $val) = each($index)) { - $result[$key] = $a[$key]; - } - } - - return $result; - } - - function fetchMIMEHeaders($mailbox, $uid, $parts, $mime=true) - { - if (!$this->select($mailbox)) { - return false; - } - - $result = false; - $parts = (array) $parts; - $key = $this->nextTag(); - $peeks = array(); - $type = $mime ? 'MIME' : 'HEADER'; - - // format request - foreach ($parts as $part) { - $peeks[] = "BODY.PEEK[$part.$type]"; - } - - $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')'; - - // send request - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - return false; - } - - do { - $line = $this->readLine(1024); - - if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) { - $idx = $matches[1]; - $headers = ''; - - // get complete entry - if (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) { - $bytes = $m[1]; - $out = ''; - - while (strlen($out) < $bytes) { - $out = $this->readBytes($bytes); - if ($out === null) - break; - $headers .= $out; - } - } - - $result[$idx] = trim($headers); - } - } while (!$this->startsWith($line, $key, true)); - - return $result; - } - - function fetchPartHeader($mailbox, $id, $is_uid=false, $part=NULL) - { - $part = empty($part) ? 'HEADER' : $part.'.MIME'; - - return $this->handlePartBody($mailbox, $id, $is_uid, $part); - } - - function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL, $formatted=true) - { - if (!$this->select($mailbox)) { - return false; - } - - switch ($encoding) { - case 'base64': - $mode = 1; - break; - case 'quoted-printable': - $mode = 2; - break; - case 'x-uuencode': - case 'x-uue': - case 'uue': - case 'uuencode': - $mode = 3; - break; - default: - $mode = 0; - } - - // Use BINARY extension when possible (and safe) - $binary = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY'); - $fetch_mode = $binary ? 'BINARY' : 'BODY'; - - // format request - $key = $this->nextTag(); - $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part])"; - - // send request - if (!$this->putLine($request)) { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - return false; - } - - if ($binary) { - $mode = -1; - } - - // receive reply line - do { - $line = rtrim($this->readLine(1024)); - $a = explode(' ', $line); - } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH'); - - $len = strlen($line); - $result = false; - - if ($a[2] != 'FETCH') { - } - // handle empty "* X FETCH ()" response - else if ($line[$len-1] == ')' && $line[$len-2] != '(') { - // one line response, get everything between first and last quotes - if (substr($line, -4, 3) == 'NIL') { - // NIL response - $result = ''; - } else { - $from = strpos($line, '"') + 1; - $to = strrpos($line, '"'); - $len = $to - $from; - $result = substr($line, $from, $len); - } - - if ($mode == 1) { - $result = base64_decode($result); - } - else if ($mode == 2) { - $result = quoted_printable_decode($result); - } - else if ($mode == 3) { - $result = convert_uudecode($result); - } - - } else if ($line[$len-1] == '}') { - // multi-line request, find sizes of content and receive that many bytes - $from = strpos($line, '{') + 1; - $to = strrpos($line, '}'); - $len = $to - $from; - $sizeStr = substr($line, $from, $len); - $bytes = (int)$sizeStr; - $prev = ''; - - while ($bytes > 0) { - $line = $this->readLine(8192); - - if ($line === NULL) { - break; - } - - $len = strlen($line); - - if ($len > $bytes) { - $line = substr($line, 0, $bytes); - $len = strlen($line); - } - $bytes -= $len; - - // BASE64 - if ($mode == 1) { - $line = rtrim($line, "\t\r\n\0\x0B"); - // create chunks with proper length for base64 decoding - $line = $prev.$line; - $length = strlen($line); - if ($length % 4) { - $length = floor($length / 4) * 4; - $prev = substr($line, $length); - $line = substr($line, 0, $length); - } - else - $prev = ''; - $line = base64_decode($line); - // QUOTED-PRINTABLE - } else if ($mode == 2) { - $line = rtrim($line, "\t\r\0\x0B"); - $line = quoted_printable_decode($line); - // UUENCODE - } else if ($mode == 3) { - $line = rtrim($line, "\t\r\n\0\x0B"); - if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) - continue; - $line = convert_uudecode($line); - // default - } else if ($formatted) { - $line = rtrim($line, "\t\r\n\0\x0B") . "\n"; - } - - if ($file) - fwrite($file, $line); - else if ($print) - echo $line; - else - $result .= $line; - } - } - - // read in anything up until last line - if (!$end) - do { - $line = $this->readLine(1024); - } while (!$this->startsWith($line, $key, true)); - - if ($result !== false) { - if ($file) { - fwrite($file, $result); - } else if ($print) { - echo $result; - } else - return $result; - return true; - } - - return false; - } - - /** - * Handler for IMAP APPEND command - * - * @param string $mailbox Mailbox name - * @param string $message Message content - * @param array $flags Message flags - * @param string $date Message internal date - * - * @return string|bool On success APPENDUID response (if available) or True, False on failure - */ - function append($mailbox, &$message, $flags = array(), $date = null) - { - unset($this->data['APPENDUID']); - - if ($mailbox === null || $mailbox === '') { - return false; - } - - $message = str_replace("\r", '', $message); - $message = str_replace("\n", "\r\n", $message); - - $len = strlen($message); - if (!$len) { - return false; - } - - // build APPEND command - $key = $this->nextTag(); - $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; - if (!empty($date)) { - $request .= ' ' . $this->escape($date); - } - $request .= ' {' . $len . ($this->prefs['literal+'] ? '+' : '') . '}'; - - // send APPEND command - if ($this->putLine($request)) { - // Do not wait when LITERAL+ is supported - if (!$this->prefs['literal+']) { - $line = $this->readReply(); - - if ($line[0] != '+') { - $this->parseResult($line, 'APPEND: '); - return false; - } - } - - if (!$this->putLine($message)) { - return false; - } - - do { - $line = $this->readLine(); - } while (!$this->startsWith($line, $key, true, true)); - - // Clear internal status cache - unset($this->data['STATUS:'.$mailbox]); - - if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) - return false; - else if (!empty($this->data['APPENDUID'])) - return $this->data['APPENDUID']; - else - return true; - } - else { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - } - - return false; - } - - /** - * Handler for IMAP APPEND command. - * - * @param string $mailbox Mailbox name - * @param string $path Path to the file with message body - * @param string $headers Message headers - * @param array $flags Message flags - * @param string $date Message internal date - * - * @return string|bool On success APPENDUID response (if available) or True, False on failure - */ - function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null) - { - unset($this->data['APPENDUID']); - - if ($mailbox === null || $mailbox === '') { - return false; - } - - // open message file - $in_fp = false; - if (file_exists(realpath($path))) { - $in_fp = fopen($path, 'r'); - } - - if (!$in_fp) { - $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading"); - return false; - } - - $body_separator = "\r\n\r\n"; - $len = filesize($path); - - if (!$len) { - return false; - } - - if ($headers) { - $headers = preg_replace('/[\r\n]+$/', '', $headers); - $len += strlen($headers) + strlen($body_separator); - } - - // build APPEND command - $key = $this->nextTag(); - $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')'; - if (!empty($date)) { - $request .= ' ' . $this->escape($date); - } - $request .= ' {' . $len . ($this->prefs['literal+'] ? '+' : '') . '}'; - - // send APPEND command - if ($this->putLine($request)) { - // Don't wait when LITERAL+ is supported - if (!$this->prefs['literal+']) { - $line = $this->readReply(); - - if ($line[0] != '+') { - $this->parseResult($line, 'APPEND: '); - return false; - } - } - - // send headers with body separator - if ($headers) { - $this->putLine($headers . $body_separator, false); - } - - // send file - while (!feof($in_fp) && $this->fp) { - $buffer = fgets($in_fp, 4096); - $this->putLine($buffer, false); - } - fclose($in_fp); - - if (!$this->putLine('')) { // \r\n - return false; - } - - // read response - do { - $line = $this->readLine(); - } while (!$this->startsWith($line, $key, true, true)); - - // Clear internal status cache - unset($this->data['STATUS:'.$mailbox]); - - if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) - return false; - else if (!empty($this->data['APPENDUID'])) - return $this->data['APPENDUID']; - else - return true; - } - else { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $request"); - } - - return false; - } - - /** - * Returns QUOTA information - * - * @return array Quota information - */ - function getQuota() - { - /* - * GETQUOTAROOT "INBOX" - * QUOTAROOT INBOX user/rchijiiwa1 - * QUOTA user/rchijiiwa1 (STORAGE 654 9765) - * OK Completed - */ - $result = false; - $quota_lines = array(); - $key = $this->nextTag(); - $command = $key . ' GETQUOTAROOT INBOX'; - - // get line(s) containing quota info - if ($this->putLine($command)) { - do { - $line = rtrim($this->readLine(5000)); - if (preg_match('/^\* QUOTA /', $line)) { - $quota_lines[] = $line; - } - } while (!$this->startsWith($line, $key, true, true)); - } - else { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $command"); - } - - // return false if not found, parse if found - $min_free = PHP_INT_MAX; - foreach ($quota_lines as $key => $quota_line) { - $quota_line = str_replace(array('(', ')'), '', $quota_line); - $parts = explode(' ', $quota_line); - $storage_part = array_search('STORAGE', $parts); - - if (!$storage_part) { - continue; - } - - $used = intval($parts[$storage_part+1]); - $total = intval($parts[$storage_part+2]); - $free = $total - $used; - - // return lowest available space from all quotas - if ($free < $min_free) { - $min_free = $free; - $result['used'] = $used; - $result['total'] = $total; - $result['percent'] = min(100, round(($used/max(1,$total))*100)); - $result['free'] = 100 - $result['percent']; - } - } - - return $result; - } - - /** - * Send the SETACL command (RFC4314) - * - * @param string $mailbox Mailbox name - * @param string $user User name - * @param mixed $acl ACL string or array - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - function setACL($mailbox, $user, $acl) - { - if (is_array($acl)) { - $acl = implode('', $acl); - } - - $result = $this->execute('SETACL', array( - $this->escape($mailbox), $this->escape($user), strtolower($acl)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the DELETEACL command (RFC4314) - * - * @param string $mailbox Mailbox name - * @param string $user User name - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - function deleteACL($mailbox, $user) - { - $result = $this->execute('DELETEACL', array( - $this->escape($mailbox), $this->escape($user)), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the GETACL command (RFC4314) - * - * @param string $mailbox Mailbox name - * - * @return array User-rights array on success, NULL on error - * @since 0.5-beta - */ - function getACL($mailbox) - { - list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox))); - - if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) { - // Parse server response (remove "* ACL ") - $response = substr($response, 6); - $ret = $this->tokenizeResponse($response); - $mbox = array_shift($ret); - $size = count($ret); - - // Create user-rights hash array - // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1 - // so we could return only standard rights defined in RFC4314, - // excluding 'c' and 'd' defined in RFC2086. - if ($size % 2 == 0) { - for ($i=0; $i<$size; $i++) { - $ret[$ret[$i]] = str_split($ret[++$i]); - unset($ret[$i-1]); - unset($ret[$i]); - } - return $ret; - } - - $this->setError(self::ERROR_COMMAND, "Incomplete ACL response"); - return NULL; - } - - return NULL; - } - - /** - * Send the LISTRIGHTS command (RFC4314) - * - * @param string $mailbox Mailbox name - * @param string $user User name - * - * @return array List of user rights - * @since 0.5-beta - */ - function listRights($mailbox, $user) - { - list($code, $response) = $this->execute('LISTRIGHTS', array( - $this->escape($mailbox), $this->escape($user))); - - if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) { - // Parse server response (remove "* LISTRIGHTS ") - $response = substr($response, 13); - - $ret_mbox = $this->tokenizeResponse($response, 1); - $ret_user = $this->tokenizeResponse($response, 1); - $granted = $this->tokenizeResponse($response, 1); - $optional = trim($response); - - return array( - 'granted' => str_split($granted), - 'optional' => explode(' ', $optional), - ); - } - - return NULL; - } - - /** - * Send the MYRIGHTS command (RFC4314) - * - * @param string $mailbox Mailbox name - * - * @return array MYRIGHTS response on success, NULL on error - * @since 0.5-beta - */ - function myRights($mailbox) - { - list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox))); - - if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) { - // Parse server response (remove "* MYRIGHTS ") - $response = substr($response, 11); - - $ret_mbox = $this->tokenizeResponse($response, 1); - $rights = $this->tokenizeResponse($response, 1); - - return str_split($rights); - } - - return NULL; - } - - /** - * Send the SETMETADATA command (RFC5464) - * - * @param string $mailbox Mailbox name - * @param array $entries Entry-value array (use NULL value as NIL) - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - function setMetadata($mailbox, $entries) - { - if (!is_array($entries) || empty($entries)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); - return false; - } - - foreach ($entries as $name => $value) { - $entries[$name] = $this->escape($name) . ' ' . $this->escape($value); - } - - $entries = implode(' ', $entries); - $result = $this->execute('SETMETADATA', array( - $this->escape($mailbox), '(' . $entries . ')'), - self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the SETMETADATA command with NIL values (RFC5464) - * - * @param string $mailbox Mailbox name - * @param array $entries Entry names array - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - function deleteMetadata($mailbox, $entries) - { - if (!is_array($entries) && !empty($entries)) { - $entries = explode(' ', $entries); - } - - if (empty($entries)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command"); - return false; - } - - foreach ($entries as $entry) { - $data[$entry] = NULL; - } - - return $this->setMetadata($mailbox, $data); - } - - /** - * Send the GETMETADATA command (RFC5464) - * - * @param string $mailbox Mailbox name - * @param array $entries Entries - * @param array $options Command options (with MAXSIZE and DEPTH keys) - * - * @return array GETMETADATA result on success, NULL on error - * - * @since 0.5-beta - */ - function getMetadata($mailbox, $entries, $options=array()) - { - if (!is_array($entries)) { - $entries = array($entries); - } - - // create entries string - foreach ($entries as $idx => $name) { - $entries[$idx] = $this->escape($name); - } - - $optlist = ''; - $entlist = '(' . implode(' ', $entries) . ')'; - - // create options string - if (is_array($options)) { - $options = array_change_key_case($options, CASE_UPPER); - $opts = array(); - - if (!empty($options['MAXSIZE'])) { - $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']); - } - if (!empty($options['DEPTH'])) { - $opts[] = 'DEPTH '.intval($options['DEPTH']); - } - - if ($opts) { - $optlist = '(' . implode(' ', $opts) . ')'; - } - } - - $optlist .= ($optlist ? ' ' : '') . $entlist; - - list($code, $response) = $this->execute('GETMETADATA', array( - $this->escape($mailbox), $optlist)); - - if ($code == self::ERROR_OK) { - $result = array(); - $data = $this->tokenizeResponse($response); - - // The METADATA response can contain multiple entries in a single - // response or multiple responses for each entry or group of entries - if (!empty($data) && ($size = count($data))) { - for ($i=0; $i<$size; $i++) { - if (isset($mbox) && is_array($data[$i])) { - $size_sub = count($data[$i]); - for ($x=0; $x<$size_sub; $x++) { - $result[$mbox][$data[$i][$x]] = $data[$i][++$x]; - } - unset($data[$i]); - } - else if ($data[$i] == '*') { - if ($data[$i+1] == 'METADATA') { - $mbox = $data[$i+2]; - unset($data[$i]); // "*" - unset($data[++$i]); // "METADATA" - unset($data[++$i]); // Mailbox - } - // get rid of other untagged responses - else { - unset($mbox); - unset($data[$i]); - } - } - else if (isset($mbox)) { - $result[$mbox][$data[$i]] = $data[++$i]; - unset($data[$i]); - unset($data[$i-1]); - } - else { - unset($data[$i]); - } - } - } - - return $result; - } - - return NULL; - } - - /** - * Send the SETANNOTATION command (draft-daboo-imap-annotatemore) - * - * @param string $mailbox Mailbox name - * @param array $data Data array where each item is an array with - * three elements: entry name, attribute name, value - * - * @return boolean True on success, False on failure - * @since 0.5-beta - */ - function setAnnotation($mailbox, $data) - { - if (!is_array($data) || empty($data)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); - return false; - } - - foreach ($data as $entry) { - // ANNOTATEMORE drafts before version 08 require quoted parameters - $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true), - $this->escape($entry[1], true), $this->escape($entry[2], true)); - } - - $entries = implode(' ', $entries); - $result = $this->execute('SETANNOTATION', array( - $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE); - - return ($result == self::ERROR_OK); - } - - /** - * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore) - * - * @param string $mailbox Mailbox name - * @param array $data Data array where each item is an array with - * two elements: entry name and attribute name - * - * @return boolean True on success, False on failure - * - * @since 0.5-beta - */ - function deleteAnnotation($mailbox, $data) - { - if (!is_array($data) || empty($data)) { - $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command"); - return false; - } - - return $this->setAnnotation($mailbox, $data); - } - - /** - * Send the GETANNOTATION command (draft-daboo-imap-annotatemore) - * - * @param string $mailbox Mailbox name - * @param array $entries Entries names - * @param array $attribs Attribs names - * - * @return array Annotations result on success, NULL on error - * - * @since 0.5-beta - */ - function getAnnotation($mailbox, $entries, $attribs) - { - if (!is_array($entries)) { - $entries = array($entries); - } - // create entries string - // ANNOTATEMORE drafts before version 08 require quoted parameters - foreach ($entries as $idx => $name) { - $entries[$idx] = $this->escape($name, true); - } - $entries = '(' . implode(' ', $entries) . ')'; - - if (!is_array($attribs)) { - $attribs = array($attribs); - } - // create entries string - foreach ($attribs as $idx => $name) { - $attribs[$idx] = $this->escape($name, true); - } - $attribs = '(' . implode(' ', $attribs) . ')'; - - list($code, $response) = $this->execute('GETANNOTATION', array( - $this->escape($mailbox), $entries, $attribs)); - - if ($code == self::ERROR_OK) { - $result = array(); - $data = $this->tokenizeResponse($response); - - // Here we returns only data compatible with METADATA result format - if (!empty($data) && ($size = count($data))) { - for ($i=0; $i<$size; $i++) { - $entry = $data[$i]; - if (isset($mbox) && is_array($entry)) { - $attribs = $entry; - $entry = $last_entry; - } - else if ($entry == '*') { - if ($data[$i+1] == 'ANNOTATION') { - $mbox = $data[$i+2]; - unset($data[$i]); // "*" - unset($data[++$i]); // "ANNOTATION" - unset($data[++$i]); // Mailbox - } - // get rid of other untagged responses - else { - unset($mbox); - unset($data[$i]); - } - continue; - } - else if (isset($mbox)) { - $attribs = $data[++$i]; - } - else { - unset($data[$i]); - continue; - } - - if (!empty($attribs)) { - for ($x=0, $len=count($attribs); $x<$len;) { - $attr = $attribs[$x++]; - $value = $attribs[$x++]; - if ($attr == 'value.priv') { - $result[$mbox]['/private' . $entry] = $value; - } - else if ($attr == 'value.shared') { - $result[$mbox]['/shared' . $entry] = $value; - } - } - } - $last_entry = $entry; - unset($data[$i]); - } - } - - return $result; - } - - return NULL; - } - - /** - * Returns BODYSTRUCTURE for the specified message. - * - * @param string $mailbox Folder name - * @param int $id Message sequence number or UID - * @param bool $is_uid True if $id is an UID - * - * @return array/bool Body structure array or False on error. - * @since 0.6 - */ - function getStructure($mailbox, $id, $is_uid = false) - { - $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE')); - if (is_array($result)) { - $result = array_shift($result); - return $result->bodystructure; - } - return false; - } - - /** - * Returns data of a message part according to specified structure. - * - * @param array $structure Message structure (getStructure() result) - * @param string $part Message part identifier - * - * @return array Part data as hash array (type, encoding, charset, size) - */ - static function getStructurePartData($structure, $part) - { - $part_a = self::getStructurePartArray($structure, $part); - $data = array(); - - if (empty($part_a)) { - return $data; - } - - // content-type - if (is_array($part_a[0])) { - $data['type'] = 'multipart'; - } - else { - $data['type'] = strtolower($part_a[0]); - - // encoding - $data['encoding'] = strtolower($part_a[5]); - - // charset - if (is_array($part_a[2])) { - while (list($key, $val) = each($part_a[2])) { - if (strcasecmp($val, 'charset') == 0) { - $data['charset'] = $part_a[2][$key+1]; - break; - } - } - } - } - - // size - $data['size'] = intval($part_a[6]); - - return $data; - } - - static function getStructurePartArray($a, $part) - { - if (!is_array($a)) { - return false; - } - - if (empty($part)) { - return $a; - } - - $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : ''; - - if (strcasecmp($ctype, 'message/rfc822') == 0) { - $a = $a[8]; - } - - if (strpos($part, '.') > 0) { - $orig_part = $part; - $pos = strpos($part, '.'); - $rest = substr($orig_part, $pos+1); - $part = substr($orig_part, 0, $pos); - - return self::getStructurePartArray($a[$part-1], $rest); - } - else if ($part > 0) { - return (is_array($a[$part-1])) ? $a[$part-1] : $a; - } - } - - /** - * Creates next command identifier (tag) - * - * @return string Command identifier - * @since 0.5-beta - */ - function nextTag() - { - $this->cmd_num++; - $this->cmd_tag = sprintf('A%04d', $this->cmd_num); - - return $this->cmd_tag; - } - - /** - * Sends IMAP command and parses result - * - * @param string $command IMAP command - * @param array $arguments Command arguments - * @param int $options Execution options - * - * @return mixed Response code or list of response code and data - * @since 0.5-beta - */ - function execute($command, $arguments=array(), $options=0) - { - $tag = $this->nextTag(); - $query = $tag . ' ' . $command; - $noresp = ($options & self::COMMAND_NORESPONSE); - $response = $noresp ? null : ''; - - if (!empty($arguments)) { - foreach ($arguments as $arg) { - $query .= ' ' . self::r_implode($arg); - } - } - - // Send command - if (!$this->putLineC($query)) { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $query"); - return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, ''); - } - - // Parse response - do { - $line = $this->readLine(4096); - if ($response !== null) { - $response .= $line; - } - } while (!$this->startsWith($line, $tag . ' ', true, true)); - - $code = $this->parseResult($line, $command . ': '); - - // Remove last line from response - if ($response) { - $line_len = min(strlen($response), strlen($line) + 2); - $response = substr($response, 0, -$line_len); - } - - // optional CAPABILITY response - if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK - && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches) - ) { - $this->parseCapability($matches[1], true); - } - - // return last line only (without command tag, result and response code) - if ($line && ($options & self::COMMAND_LASTLINE)) { - $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line)); - } - - return $noresp ? $code : array($code, $response); - } - - /** - * Splits IMAP response into string tokens - * - * @param string &$str The IMAP's server response - * @param int $num Number of tokens to return - * - * @return mixed Tokens array or string if $num=1 - * @since 0.5-beta - */ - static function tokenizeResponse(&$str, $num=0) - { - $result = array(); - - while (!$num || count($result) < $num) { - // remove spaces from the beginning of the string - $str = ltrim($str); - - switch ($str[0]) { - - // String literal - case '{': - if (($epos = strpos($str, "}\r\n", 1)) == false) { - // error - } - if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) { - // error - } - $result[] = $bytes ? substr($str, $epos + 3, $bytes) : ''; - // Advance the string - $str = substr($str, $epos + 3 + $bytes); - break; - - // Quoted string - case '"': - $len = strlen($str); - - for ($pos=1; $pos<$len; $pos++) { - if ($str[$pos] == '"') { - break; - } - if ($str[$pos] == "\\") { - if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") { - $pos++; - } - } - } - if ($str[$pos] != '"') { - // error - } - // we need to strip slashes for a quoted string - $result[] = stripslashes(substr($str, 1, $pos - 1)); - $str = substr($str, $pos + 1); - break; - - // Parenthesized list - case '(': - case '[': - $str = substr($str, 1); - $result[] = self::tokenizeResponse($str); - break; - case ')': - case ']': - $str = substr($str, 1); - return $result; - break; - - // String atom, number, NIL, *, % - default: - // empty string - if ($str === '' || $str === null) { - break 2; - } - - // excluded chars: SP, CTL, ), [, ] - if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) { - $result[] = $m[1] == 'NIL' ? NULL : $m[1]; - $str = substr($str, strlen($m[1])); - } - break; - } - } - - return $num == 1 ? $result[0] : $result; - } - - static function r_implode($element) - { - $string = ''; - - if (is_array($element)) { - reset($element); - while (list($key, $value) = each($element)) { - $string .= ' ' . self::r_implode($value); - } - } - else { - return $element; - } - - return '(' . trim($string) . ')'; - } - - /** - * Converts message identifiers array into sequence-set syntax - * - * @param array $messages Message identifiers - * @param bool $force Forces compression of any size - * - * @return string Compressed sequence-set - */ - static function compressMessageSet($messages, $force=false) - { - // given a comma delimited list of independent mid's, - // compresses by grouping sequences together - - if (!is_array($messages)) { - // if less than 255 bytes long, let's not bother - if (!$force && strlen($messages)<255) { - return $messages; - } - - // see if it's already been compressed - if (strpos($messages, ':') !== false) { - return $messages; - } - - // separate, then sort - $messages = explode(',', $messages); - } - - sort($messages); - - $result = array(); - $start = $prev = $messages[0]; - - foreach ($messages as $id) { - $incr = $id - $prev; - if ($incr > 1) { // found a gap - if ($start == $prev) { - $result[] = $prev; // push single id - } else { - $result[] = $start . ':' . $prev; // push sequence as start_id:end_id - } - $start = $id; // start of new sequence - } - $prev = $id; - } - - // handle the last sequence/id - if ($start == $prev) { - $result[] = $prev; - } else { - $result[] = $start.':'.$prev; - } - - // return as comma separated string - return implode(',', $result); - } - - /** - * Converts message sequence-set into array - * - * @param string $messages Message identifiers - * - * @return array List of message identifiers - */ - static function uncompressMessageSet($messages) - { - $result = array(); - $messages = explode(',', $messages); - - foreach ($messages as $idx => $part) { - $items = explode(':', $part); - $max = max($items[0], $items[1]); - - for ($x=$items[0]; $x<=$max; $x++) { - $result[] = $x; - } - unset($messages[$idx]); - } - - return $result; - } - - private function _xor($string, $string2) - { - $result = ''; - $size = strlen($string); - - for ($i=0; $i<$size; $i++) { - $result .= chr(ord($string[$i]) ^ ord($string2[$i])); - } - - return $result; - } - - /** - * Converts flags array into string for inclusion in IMAP command - * - * @param array $flags Flags (see self::flags) - * - * @return string Space-separated list of flags - */ - private function flagsToStr($flags) - { - foreach ((array)$flags as $idx => $flag) { - if ($flag = $this->flags[strtoupper($flag)]) { - $flags[$idx] = $flag; - } - } - - return implode(' ', (array)$flags); - } - - /** - * Converts datetime string into unix timestamp - * - * @param string $date Date string - * - * @return int Unix timestamp - */ - static function strToTime($date) - { - // support non-standard "GMTXXXX" literal - $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); - - // if date parsing fails, we have a date in non-rfc format - // remove token from the end and try again - while (($ts = intval(@strtotime($date))) <= 0) { - $d = explode(' ', $date); - array_pop($d); - if (empty($d)) { - break; - } - $date = implode(' ', $d); - } - - return $ts < 0 ? 0 : $ts; - } - - /** - * CAPABILITY response parser - */ - private function parseCapability($str, $trusted=false) - { - $str = preg_replace('/^\* CAPABILITY /i', '', $str); - - $this->capability = explode(' ', strtoupper($str)); - - if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) { - $this->prefs['literal+'] = true; - } - - if ($trusted) { - $this->capability_readed = true; - } - } - - /** - * Escapes a string when it contains special characters (RFC3501) - * - * @param string $string IMAP string - * @param boolean $force_quotes Forces string quoting (for atoms) - * - * @return string String atom, quoted-string or string literal - * @todo lists - */ - static function escape($string, $force_quotes=false) - { - if ($string === null) { - return 'NIL'; - } - - if ($string === '') { - return '""'; - } - - // atom-string (only safe characters) - if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) { - return $string; - } - - // quoted-string - if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) { - return '"' . addcslashes($string, '\\"') . '"'; - } - - // literal-string - return sprintf("{%d}\r\n%s", strlen($string), $string); - } - - /** - * Unescapes quoted-string - * - * @param string $string IMAP string - * - * @return string String - */ - static function unEscape($string) - { - return stripslashes($string); - } - - /** - * Set the value of the debugging flag. - * - * @param boolean $debug New value for the debugging flag. - * - * @since 0.5-stable - */ - function setDebug($debug, $handler = null) - { - $this->_debug = $debug; - $this->_debug_handler = $handler; - } - - /** - * Write the given debug text to the current debug output handler. - * - * @param string $message Debug mesage text. - * - * @since 0.5-stable - */ - private function debug($message) - { - if ($this->resourceid) { - $message = sprintf('[%s] %s', $this->resourceid, $message); - } - - if ($this->_debug_handler) { - call_user_func_array($this->_debug_handler, array(&$this, $message)); - } else { - echo "DEBUG: $message\n"; - } - } - -} diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php deleted file mode 100644 index 61a073fa3..000000000 --- a/program/include/rcube_ldap.php +++ /dev/null @@ -1,2323 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_ldap.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2012, The Roundcube Dev Team | - | Copyright (C) 2011, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Interface to an LDAP address directory | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Andreas Dick <andudi (at) gmx (dot) ch> | - | Aleksander Machniak <machniak@kolabsys.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Model class to access an LDAP address directory - * - * @package Addressbook - */ -class rcube_ldap extends rcube_addressbook -{ - /** public properties */ - public $primary_key = 'ID'; - public $groups = false; - public $readonly = true; - public $ready = false; - public $group_id = 0; - public $coltypes = array(); - - /** private properties */ - protected $conn; - protected $prop = array(); - protected $fieldmap = array(); - protected $sub_filter; - protected $filter = ''; - protected $result = null; - protected $ldap_result = null; - protected $mail_domain = ''; - protected $debug = false; - - private $base_dn = ''; - private $groups_base_dn = ''; - private $group_url = null; - private $cache; - - private $vlv_active = false; - private $vlv_count = 0; - - - /** - * Object constructor - * - * @param array $p LDAP connection properties - * @param boolean $debug Enables debug mode - * @param string $mail_domain Current user mail domain name - */ - function __construct($p, $debug = false, $mail_domain = null) - { - $this->prop = $p; - - if (isset($p['searchonly'])) - $this->searchonly = $p['searchonly']; - - // check if groups are configured - if (is_array($p['groups']) && count($p['groups'])) { - $this->groups = true; - // set member field - if (!empty($p['groups']['member_attr'])) - $this->prop['member_attr'] = strtolower($p['groups']['member_attr']); - else if (empty($p['member_attr'])) - $this->prop['member_attr'] = 'member'; - // set default name attribute to cn - if (empty($this->prop['groups']['name_attr'])) - $this->prop['groups']['name_attr'] = 'cn'; - if (empty($this->prop['groups']['scope'])) - $this->prop['groups']['scope'] = 'sub'; - } - - // fieldmap property is given - if (is_array($p['fieldmap'])) { - foreach ($p['fieldmap'] as $rf => $lf) - $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf)); - } - else if (!empty($p)) { - // read deprecated *_field properties to remain backwards compatible - foreach ($p as $prop => $value) - if (preg_match('/^(.+)_field$/', $prop, $matches)) - $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value)); - } - - // use fieldmap to advertise supported coltypes to the application - foreach ($this->fieldmap as $colv => $lfv) { - list($col, $type) = explode(':', $colv); - list($lf, $limit, $delim) = explode(':', $lfv); - - if ($limit == '*') $limit = null; - else $limit = max(1, intval($limit)); - - if (!is_array($this->coltypes[$col])) { - $subtypes = $type ? array($type) : null; - $this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf)); - } - elseif ($type) { - $this->coltypes[$col]['subtypes'][] = $type; - $this->coltypes[$col]['attributes'][] = $lf; - $this->coltypes[$col]['limit'] += $limit; - } - - if ($delim) - $this->coltypes[$col]['serialized'][$type] = $delim; - - $this->fieldmap[$colv] = $lf; - } - - // support for composite address - if ($this->coltypes['street'] && $this->coltypes['locality']) { - $this->coltypes['address'] = array( - 'limit' => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']), - 'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']), - 'childs' => array(), - ) + (array)$this->coltypes['address']; - - foreach (array('street','locality','zipcode','region','country') as $childcol) { - if ($this->coltypes[$childcol]) { - $this->coltypes['address']['childs'][$childcol] = array('type' => 'text'); - unset($this->coltypes[$childcol]); // remove address child col from global coltypes list - } - } - - // at least one address type must be specified - if (empty($this->coltypes['address']['subtypes'])) { - $this->coltypes['address']['subtypes'] = array('home'); - } - } - else if ($this->coltypes['address']) { - $this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40); - - // 'serialized' means the UI has to present a composite address field - if ($this->coltypes['address']['serialized']) { - $childprop = array('type' => 'text'); - $this->coltypes['address']['type'] = 'composite'; - $this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop); - } - } - - // make sure 'required_fields' is an array - if (!is_array($this->prop['required_fields'])) { - $this->prop['required_fields'] = (array) $this->prop['required_fields']; - } - - // make sure LDAP_rdn field is required - if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])) { - $this->prop['required_fields'][] = $this->prop['LDAP_rdn']; - } - - foreach ($this->prop['required_fields'] as $key => $val) { - $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val)); - } - - // Build sub_fields filter - if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) { - $this->sub_filter = ''; - foreach ($this->prop['sub_fields'] as $attr => $class) { - if (!empty($class)) { - $class = is_array($class) ? array_pop($class) : $class; - $this->sub_filter .= '(objectClass=' . $class . ')'; - } - } - if (count($this->prop['sub_fields']) > 1) { - $this->sub_filter = '(|' . $this->sub_filter . ')'; - } - } - - $this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort']; - $this->debug = $debug; - $this->mail_domain = $mail_domain; - - // initialize cache - $rcube = rcube::get_instance(); - $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600); - - $this->_connect(); - } - - - /** - * Establish a connection to the LDAP server - */ - private function _connect() - { - $rcube = rcube::get_instance(); - - if (!function_exists('ldap_connect')) - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "No ldap support in this installation of PHP"), - true, true); - - if (is_resource($this->conn)) - return true; - - if (!is_array($this->prop['hosts'])) - $this->prop['hosts'] = array($this->prop['hosts']); - - if (empty($this->prop['ldap_version'])) - $this->prop['ldap_version'] = 3; - - foreach ($this->prop['hosts'] as $host) - { - $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); - $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : ''); - - $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]"); - - if ($lc = @ldap_connect($host, $this->prop['port'])) - { - if ($this->prop['use_tls'] === true) - if (!ldap_start_tls($lc)) - continue; - - $this->_debug("S: OK"); - - ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']); - $this->prop['host'] = $host; - $this->conn = $lc; - - if (isset($this->prop['referrals'])) - ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']); - break; - } - $this->_debug("S: NOT OK"); - } - - // See if the directory is writeable. - if ($this->prop['writable']) { - $this->readonly = false; - } - - if (!is_resource($this->conn)) { - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not connect to any LDAP server, last tried $hostname"), true); - - return false; - } - - $bind_pass = $this->prop['bind_pass']; - $bind_user = $this->prop['bind_user']; - $bind_dn = $this->prop['bind_dn']; - - $this->base_dn = $this->prop['base_dn']; - $this->groups_base_dn = ($this->prop['groups']['base_dn']) ? - $this->prop['groups']['base_dn'] : $this->base_dn; - - // User specific access, generate the proper values to use. - if ($this->prop['user_specific']) { - // No password set, use the session password - if (empty($bind_pass)) { - $bind_pass = $rcube->decrypt($_SESSION['password']); - } - - // Get the pieces needed for variable replacement. - if ($fu = $rcube->get_user_name()) - list($u, $d) = explode('@', $fu); - else - $d = $this->mail_domain; - - $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string - - $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); - - if ($this->prop['search_base_dn'] && $this->prop['search_filter']) { - if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) { - $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']); - } - - // Search for the dn to use to authenticate - $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces); - $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces); - - $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}"); - - $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid')); - if ($res) { - if (($entry = ldap_first_entry($this->conn, $res)) - && ($bind_dn = ldap_get_dn($this->conn, $entry)) - ) { - $this->_debug("S: search returned dn: $bind_dn"); - $dn = ldap_explode_dn($bind_dn, 1); - $replaces['%dn'] = $dn[0]; - } - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - - // DN not found - if (empty($replaces['%dn'])) { - if (!empty($this->prop['search_dn_default'])) - $replaces['%dn'] = $this->prop['search_dn_default']; - else { - rcube::raise_error(array( - 'code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "DN not found using LDAP search."), true); - return false; - } - } - } - - // Replace the bind_dn and base_dn variables. - $bind_dn = strtr($bind_dn, $replaces); - $this->base_dn = strtr($this->base_dn, $replaces); - $this->groups_base_dn = strtr($this->groups_base_dn, $replaces); - - if (empty($bind_user)) { - $bind_user = $u; - } - } - - if (empty($bind_pass)) { - $this->ready = true; - } - else { - if (!empty($bind_dn)) { - $this->ready = $this->bind($bind_dn, $bind_pass); - } - else if (!empty($this->prop['auth_cid'])) { - $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user); - } - else { - $this->ready = $this->sasl_bind($bind_user, $bind_pass); - } - } - - return $this->ready; - } - - - /** - * Bind connection with (SASL-) user and password - * - * @param string $authc Authentication user - * @param string $pass Bind password - * @param string $authz Autorization user - * - * @return boolean True on success, False on error - */ - public function sasl_bind($authc, $pass, $authz=null) - { - if (!$this->conn) { - return false; - } - - if (!function_exists('ldap_sasl_bind')) { - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Unable to bind: ldap_sasl_bind() not exists"), - true, true); - } - - if (!empty($authz)) { - $authz = 'u:' . $authz; - } - - if (!empty($this->prop['auth_method'])) { - $method = $this->prop['auth_method']; - } - else { - $method = 'DIGEST-MD5'; - } - - $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]"); - - if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) { - $this->_debug("S: OK"); - return true; - } - - $this->_debug("S: ".ldap_error($this->conn)); - - rcube::raise_error(array( - 'code' => ldap_errno($this->conn), 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)), - true); - - return false; - } - - - /** - * Bind connection with DN and password - * - * @param string Bind DN - * @param string Bind password - * - * @return boolean True on success, False on error - */ - public function bind($dn, $pass) - { - if (!$this->conn) { - return false; - } - - $this->_debug("C: Bind [dn: $dn] [pass: $pass]"); - - if (@ldap_bind($this->conn, $dn, $pass)) { - $this->_debug("S: OK"); - return true; - } - - $this->_debug("S: ".ldap_error($this->conn)); - - rcube::raise_error(array( - 'code' => ldap_errno($this->conn), 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)), - true); - - return false; - } - - - /** - * Close connection to LDAP server - */ - function close() - { - if ($this->conn) - { - $this->_debug("C: Close"); - ldap_unbind($this->conn); - $this->conn = null; - } - } - - - /** - * Returns address book name - * - * @return string Address book name - */ - function get_name() - { - return $this->prop['name']; - } - - - /** - * Set internal sort settings - * - * @param string $sort_col Sort column - * @param string $sort_order Sort order - */ - function set_sort_order($sort_col, $sort_order = null) - { - if ($this->coltypes[$sort_col]['attributes']) - $this->sort_col = $this->coltypes[$sort_col]['attributes'][0]; - } - - - /** - * Save a search string for future listings - * - * @param string $filter Filter string - */ - function set_search_set($filter) - { - $this->filter = $filter; - } - - - /** - * Getter for saved search properties - * - * @return mixed Search properties used by this class - */ - function get_search_set() - { - return $this->filter; - } - - - /** - * Reset all saved results and search parameters - */ - function reset() - { - $this->result = null; - $this->ldap_result = null; - $this->filter = ''; - } - - - /** - * List the current set of contact records - * - * @param array List of cols to show - * @param int Only return this number of records - * - * @return array Indexed list of contact records, each a hash array - */ - function list_records($cols=null, $subset=0) - { - if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) - { - $this->result = new rcube_result_set(0); - $this->result->searchonly = true; - return $this->result; - } - - // fetch group members recursively - if ($this->group_id && $this->group_data['dn']) - { - $entries = $this->list_group_members($this->group_data['dn']); - - // make list of entries unique and sort it - $seen = array(); - foreach ($entries as $i => $rec) { - if ($seen[$rec['dn']]++) - unset($entries[$i]); - } - usort($entries, array($this, '_entry_sort_cmp')); - - $entries['count'] = count($entries); - $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size); - } - else - { - // add general filter to query - if (!empty($this->prop['filter']) && empty($this->filter)) - $this->set_search_set($this->prop['filter']); - - // exec LDAP search if no result resource is stored - if ($this->conn && !$this->ldap_result) - $this->_exec_search(); - - // count contacts for this user - $this->result = $this->count(); - - // we have a search result resource - if ($this->ldap_result && $this->result->count > 0) - { - // sorting still on the ldap server - if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active) - ldap_sort($this->conn, $this->ldap_result, $this->sort_col); - - // get all entries from the ldap server - $entries = ldap_get_entries($this->conn, $this->ldap_result); - } - - } // end else - - // start and end of the page - $start_row = $this->vlv_active ? 0 : $this->result->first; - $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row; - $last_row = $this->result->first + $this->page_size; - $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row; - - // filter entries for this page - for ($i = $start_row; $i < min($entries['count'], $last_row); $i++) - $this->result->add($this->_ldap2result($entries[$i])); - - return $this->result; - } - - /** - * Get all members of the given group - * - * @param string Group DN - * @param array Group entries (if called recursively) - * @return array Accumulated group members - */ - function list_group_members($dn, $count = false, $entries = null) - { - $group_members = array(); - - // fetch group object - if (empty($entries)) { - $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL')); - if ($result === false) - { - $this->_debug("S: ".ldap_error($this->conn)); - return $group_members; - } - - $entries = @ldap_get_entries($this->conn, $result); - } - - for ($i=0; $i < $entries['count']; $i++) - { - $entry = $entries[$i]; - - if (empty($entry['objectclass'])) - continue; - - foreach ((array)$entry['objectclass'] as $objectclass) - { - switch (strtolower($objectclass)) { - case "group": - case "groupofnames": - case "kolabgroupofnames": - $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count)); - break; - case "groupofuniquenames": - case "kolabgroupofuniquenames": - $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count)); - break; - case "groupofurls": - $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count)); - break; - } - } - - if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) - break; - } - - return array_filter($group_members); - } - - /** - * Fetch members of the given group entry from server - * - * @param string Group DN - * @param array Group entry - * @param string Member attribute to use - * @return array Accumulated group members - */ - private function _list_group_members($dn, $entry, $attr, $count) - { - // Use the member attributes to return an array of member ldap objects - // NOTE that the member attribute is supposed to contain a DN - $group_members = array(); - if (empty($entry[$attr])) - return $group_members; - - // read these attributes for all members - $attrib = $count ? array('dn') : array_values($this->fieldmap); - $attrib[] = 'objectClass'; - $attrib[] = 'member'; - $attrib[] = 'uniqueMember'; - $attrib[] = 'memberURL'; - - for ($i=0; $i < $entry[$attr]['count']; $i++) - { - if (empty($entry[$attr][$i])) - continue; - - $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)', - $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']); - - $members = @ldap_get_entries($this->conn, $result); - if ($members == false) - { - $this->_debug("S: ".ldap_error($this->conn)); - $members = array(); - } - - // for nested groups, call recursively - $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members); - - unset($members['count']); - $group_members = array_merge($group_members, array_filter($members), $nested_group_members); - } - - return $group_members; - } - - /** - * List members of group class groupOfUrls - * - * @param string Group DN - * @param array Group entry - * @param boolean True if only used for counting - * @return array Accumulated group members - */ - private function _list_group_memberurl($dn, $entry, $count) - { - $group_members = array(); - - for ($i=0; $i < $entry['memberurl']['count']; $i++) - { - // extract components from url - if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m)) - continue; - - // add search filter if any - $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3]; - $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list'); - - $attrib = $count ? array('dn') : array_values($this->fieldmap); - if ($result = @$func($this->conn, $m[1], $filter, - $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']) - ) { - $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]); - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - return $group_members; - } - - $entries = @ldap_get_entries($this->conn, $result); - for ($j = 0; $j < $entries['count']; $j++) - { - if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)) - $group_members = array_merge($group_members, $nested_group_members); - else - $group_members[] = $entries[$j]; - } - } - - return $group_members; - } - - /** - * Callback for sorting entries - */ - function _entry_sort_cmp($a, $b) - { - return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]); - } - - - /** - * Search contacts - * - * @param mixed $fields The field name of array of field names to search in - * @param mixed $value Search value (or array of values when $fields is array) - * @param int $mode Matching mode: - * 0 - partial (*abc*), - * 1 - strict (=), - * 2 - prefix (abc*) - * @param boolean $select True if results are requested, False if count only - * @param boolean $nocount (Not used) - * @param array $required List of fields that cannot be empty - * - * @return array Indexed list of contact records and 'count' value - */ - function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()) - { - $mode = intval($mode); - - // special treatment for ID-based search - if ($fields == 'ID' || $fields == $this->primary_key) - { - $ids = !is_array($value) ? explode(',', $value) : $value; - $result = new rcube_result_set(); - foreach ($ids as $id) - { - if ($rec = $this->get_record($id, true)) - { - $result->add($rec); - $result->count++; - } - } - return $result; - } - - // use VLV pseudo-search for autocompletion - $rcube = rcube::get_instance(); - $list_fields = $rcube->config->get('contactlist_fields'); - - if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields)) - { - // add general filter to query - if (!empty($this->prop['filter']) && empty($this->filter)) - $this->set_search_set($this->prop['filter']); - - // set VLV controls with encoded search string - $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value); - - $function = $this->_scope2func($this->prop['scope']); - $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)', - array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']); - - $this->result = new rcube_result_set(0); - - if (!$this->ldap_result) { - $this->_debug("S: ".ldap_error($this->conn)); - return $this->result; - } - - $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)"); - - // get all entries of this page and post-filter those that really match the query - $search = mb_strtolower($value); - $entries = ldap_get_entries($this->conn, $this->ldap_result); - - for ($i = 0; $i < $entries['count']; $i++) { - $rec = $this->_ldap2result($entries[$i]); - foreach ($fields as $f) { - foreach ((array)$rec[$f] as $val) { - $val = mb_strtolower($val); - switch ($mode) { - case 1: - $got = ($val == $search); - break; - case 2: - $got = ($search == substr($val, 0, strlen($search))); - break; - default: - $got = (strpos($val, $search) !== false); - break; - } - - if ($got) { - $this->result->add($rec); - $this->result->count++; - break 2; - } - } - } - } - - return $this->result; - } - - // use AND operator for advanced searches - $filter = is_array($value) ? '(&' : '(|'; - // set wildcards - $wp = $ws = ''; - if (!empty($this->prop['fuzzy_search']) && $mode != 1) { - $ws = '*'; - if (!$mode) { - $wp = '*'; - } - } - - if ($fields == '*') - { - // search_fields are required for fulltext search - if (empty($this->prop['search_fields'])) - { - $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch'); - $this->result = new rcube_result_set(); - return $this->result; - } - if (is_array($this->prop['search_fields'])) - { - foreach ($this->prop['search_fields'] as $field) { - $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)"; - } - } - } - else - { - foreach ((array)$fields as $idx => $field) { - $val = is_array($value) ? $value[$idx] : $value; - if ($attrs = $this->_map_field($field)) { - if (count($attrs) > 1) - $filter .= '(|'; - foreach ($attrs as $f) - $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)"; - if (count($attrs) > 1) - $filter .= ')'; - } - } - } - $filter .= ')'; - - // add required (non empty) fields filter - $req_filter = ''; - foreach ((array)$required as $field) { - if ($attrs = $this->_map_field($field)) { - if (count($attrs) > 1) - $req_filter .= '(|'; - foreach ($attrs as $f) - $req_filter .= "($f=*)"; - if (count($attrs) > 1) - $req_filter .= ')'; - } - } - - if (!empty($req_filter)) - $filter = '(&' . $req_filter . $filter . ')'; - - // avoid double-wildcard if $value is empty - $filter = preg_replace('/\*+/', '*', $filter); - - // add general filter to query - if (!empty($this->prop['filter'])) - $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')'; - - // set filter string and execute search - $this->set_search_set($filter); - $this->_exec_search(); - - if ($select) - $this->list_records(); - else - $this->result = $this->count(); - - return $this->result; - } - - - /** - * Count number of available contacts in database - * - * @return object rcube_result_set Resultset with values for 'count' and 'first' - */ - function count() - { - $count = 0; - if ($this->conn && $this->ldap_result) { - $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result); - } - else if ($this->group_id && $this->group_data['dn']) { - $count = count($this->list_group_members($this->group_data['dn'], true)); - } - else if ($this->conn) { - // We have a connection but no result set, attempt to get one. - if (empty($this->filter)) { - // The filter is not set, set it. - $this->filter = $this->prop['filter']; - } - - $count = (int) $this->_exec_search(true); - } - - return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); - } - - - /** - * Return the last result set - * - * @return object rcube_result_set Current resultset or NULL if nothing selected yet - */ - function get_result() - { - return $this->result; - } - - - /** - * Get a specific contact record - * - * @param mixed Record identifier - * @param boolean Return as associative array - * - * @return mixed Hash array or rcube_result_set with all record fields - */ - function get_record($dn, $assoc=false) - { - $res = $this->result = null; - - if ($this->conn && $dn) - { - $dn = self::dn_decode($dn); - - $this->_debug("C: Read [dn: $dn] [(objectclass=*)]"); - - if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) { - $this->_debug("S: OK"); - - $entry = ldap_first_entry($this->conn, $ldap_result); - - if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) { - $rec = array_change_key_case($rec, CASE_LOWER); - } - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - - // Use ldap_list to get subentries like country (c) attribute (#1488123) - if (!empty($rec) && $this->sub_filter) { - if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) { - foreach ($entries as $entry) { - $lrec = array_change_key_case($entry, CASE_LOWER); - $rec = array_merge($lrec, $rec); - } - } - } - - if (!empty($rec)) { - // Add in the dn for the entry. - $rec['dn'] = $dn; - $res = $this->_ldap2result($rec); - $this->result = new rcube_result_set(); - $this->result->add($res); - } - } - - return $assoc ? $res : $this->result; - } - - - /** - * Check the given data before saving. - * If input not valid, the message to display can be fetched using get_error() - * - * @param array Assoziative array with data to save - * @param boolean Try to fix/complete record automatically - * @return boolean True if input is valid, False if not. - */ - public function validate(&$save_data, $autofix = false) - { - // validate e-mail addresses - if (!parent::validate($save_data, $autofix)) { - return false; - } - - // check for name input - if (empty($save_data['name'])) { - $this->set_error(self::ERROR_VALIDATE, 'nonamewarning'); - return false; - } - - // Verify that the required fields are set. - $missing = null; - $ldap_data = $this->_map_data($save_data); - foreach ($this->prop['required_fields'] as $fld) { - if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') { - $missing[$fld] = 1; - } - } - - if ($missing) { - // try to complete record automatically - if ($autofix) { - $sn_field = $this->fieldmap['surname']; - $fn_field = $this->fieldmap['firstname']; - $mail_field = $this->fieldmap['email']; - - // try to extract surname and firstname from displayname - $reverse_map = array_flip($this->fieldmap); - $name_parts = preg_split('/[\s,.]+/', $save_data['name']); - - if ($sn_field && $missing[$sn_field]) { - $save_data['surname'] = array_pop($name_parts); - unset($missing[$sn_field]); - } - - if ($fn_field && $missing[$fn_field]) { - $save_data['firstname'] = array_shift($name_parts); - unset($missing[$fn_field]); - } - - // try to fix missing e-mail, very often on import - // from vCard we have email:other only defined - if ($mail_field && $missing[$mail_field]) { - $emails = $this->get_col_values('email', $save_data, true); - if (!empty($emails) && ($email = array_shift($emails))) { - $save_data['email'] = $email; - unset($missing[$mail_field]); - } - } - } - - // TODO: generate message saying which fields are missing - if (!empty($missing)) { - $this->set_error(self::ERROR_VALIDATE, 'formincomplete'); - return false; - } - } - - return true; - } - - - /** - * Create a new contact record - * - * @param array Hash array with save data - * - * @return encoded record ID on success, False on error - */ - function insert($save_cols) - { - // Map out the column names to their LDAP ones to build the new entry. - $newentry = $this->_map_data($save_cols); - $newentry['objectClass'] = $this->prop['LDAP_Object_Classes']; - - // Verify that the required fields are set. - $missing = null; - foreach ($this->prop['required_fields'] as $fld) { - if (!isset($newentry[$fld])) { - $missing[] = $fld; - } - } - - // abort process if requiered fields are missing - // TODO: generate message saying which fields are missing - if ($missing) { - $this->set_error(self::ERROR_VALIDATE, 'formincomplete'); - return false; - } - - // Build the new entries DN. - $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn; - - // Remove attributes that need to be added separately (child objects) - $xfields = array(); - if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) { - foreach ($this->prop['sub_fields'] as $xf => $xclass) { - if (!empty($newentry[$xf])) { - $xfields[$xf] = $newentry[$xf]; - unset($newentry[$xf]); - } - } - } - - if (!$this->ldap_add($dn, $newentry)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - - foreach ($xfields as $xidx => $xf) { - $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn; - $xf = array( - $xidx => $xf, - 'objectClass' => (array) $this->prop['sub_fields'][$xidx], - ); - - $this->ldap_add($xdn, $xf); - } - - $dn = self::dn_encode($dn); - - // add new contact to the selected group - if ($this->group_id) - $this->add_to_group($this->group_id, $dn); - - return $dn; - } - - - /** - * Update a specific contact record - * - * @param mixed Record identifier - * @param array Hash array with save data - * - * @return boolean True on success, False on error - */ - function update($id, $save_cols) - { - $record = $this->get_record($id, true); - - $newdata = array(); - $replacedata = array(); - $deletedata = array(); - $subdata = array(); - $subdeldata = array(); - $subnewdata = array(); - - $ldap_data = $this->_map_data($save_cols); - $old_data = $record['_raw_attrib']; - - // special handling of photo col - if ($photo_fld = $this->fieldmap['photo']) { - // undefined means keep old photo - if (!array_key_exists('photo', $save_cols)) { - $ldap_data[$photo_fld] = $record['photo']; - } - } - - foreach ($this->fieldmap as $col => $fld) { - if ($fld) { - $val = $ldap_data[$fld]; - $old = $old_data[$fld]; - // remove empty array values - if (is_array($val)) - $val = array_filter($val); - // $this->_map_data() result and _raw_attrib use different format - // make sure comparing array with one element with a string works as expected - if (is_array($old) && count($old) == 1 && !is_array($val)) { - $old = array_pop($old); - } - if (is_array($val) && count($val) == 1 && !is_array($old)) { - $val = array_pop($val); - } - // Subentries must be handled separately - if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) { - if ($old != $val) { - if ($old !== null) { - $subdeldata[$fld] = $old; - } - if ($val) { - $subnewdata[$fld] = $val; - } - } - else if ($old !== null) { - $subdata[$fld] = $old; - } - continue; - } - - // The field does exist compare it to the ldap record. - if ($old != $val) { - // Changed, but find out how. - if ($old === null) { - // Field was not set prior, need to add it. - $newdata[$fld] = $val; - } - else if ($val == '') { - // Field supplied is empty, verify that it is not required. - if (!in_array($fld, $this->prop['required_fields'])) { - // ...It is not, safe to clear. - // #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..." - // jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too - $deletedata[$fld] = array(); - //$deletedata[$fld] = $old_data[$fld]; - } - } - else { - // The data was modified, save it out. - $replacedata[$fld] = $val; - } - } // end if - } // end if - } // end foreach - - // console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata); - - $dn = self::dn_decode($id); - - // Update the entry as required. - if (!empty($deletedata)) { - // Delete the fields. - if (!$this->ldap_mod_del($dn, $deletedata)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - } // end if - - if (!empty($replacedata)) { - // Handle RDN change - if ($replacedata[$this->prop['LDAP_rdn']]) { - $newdn = $this->prop['LDAP_rdn'].'=' - .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true) - .','.$this->base_dn; - if ($dn != $newdn) { - $newrdn = $this->prop['LDAP_rdn'].'=' - .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true); - unset($replacedata[$this->prop['LDAP_rdn']]); - } - } - // Replace the fields. - if (!empty($replacedata)) { - if (!$this->ldap_mod_replace($dn, $replacedata)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - } - } // end if - - // RDN change, we need to remove all sub-entries - if (!empty($newrdn)) { - $subdeldata = array_merge($subdeldata, $subdata); - $subnewdata = array_merge($subnewdata, $subdata); - } - - // remove sub-entries - if (!empty($subdeldata)) { - foreach ($subdeldata as $fld => $val) { - $subdn = $fld.'='.$this->_quote_string($val).','.$dn; - if (!$this->ldap_delete($subdn)) { - return false; - } - } - } - - if (!empty($newdata)) { - // Add the fields. - if (!$this->ldap_mod_add($dn, $newdata)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - } // end if - - // Handle RDN change - if (!empty($newrdn)) { - if (!$this->ldap_rename($dn, $newrdn, null, true)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - - $dn = self::dn_encode($dn); - $newdn = self::dn_encode($newdn); - - // change the group membership of the contact - if ($this->groups) { - $group_ids = $this->get_record_groups($dn); - foreach ($group_ids as $group_id) - { - $this->remove_from_group($group_id, $dn); - $this->add_to_group($group_id, $newdn); - } - } - - $dn = self::dn_decode($newdn); - } - - // add sub-entries - if (!empty($subnewdata)) { - foreach ($subnewdata as $fld => $val) { - $subdn = $fld.'='.$this->_quote_string($val).','.$dn; - $xf = array( - $fld => $val, - 'objectClass' => (array) $this->prop['sub_fields'][$fld], - ); - $this->ldap_add($subdn, $xf); - } - } - - return $newdn ? $newdn : true; - } - - - /** - * Mark one or more contact records as deleted - * - * @param array Record identifiers - * @param boolean Remove record(s) irreversible (unsupported) - * - * @return boolean True on success, False on error - */ - function delete($ids, $force=true) - { - if (!is_array($ids)) { - // Not an array, break apart the encoded DNs. - $ids = explode(',', $ids); - } // end if - - foreach ($ids as $id) { - $dn = self::dn_decode($id); - - // Need to delete all sub-entries first - if ($this->sub_filter) { - if ($entries = $this->ldap_list($dn, $this->sub_filter)) { - foreach ($entries as $entry) { - if (!$this->ldap_delete($entry['dn'])) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - } - } - } - - // Delete the record. - if (!$this->ldap_delete($dn)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - - // remove contact from all groups where he was member - if ($this->groups) { - $dn = self::dn_encode($dn); - $group_ids = $this->get_record_groups($dn); - foreach ($group_ids as $group_id) { - $this->remove_from_group($group_id, $dn); - } - } - } // end foreach - - return count($ids); - } - - - /** - * Remove all contact records - */ - function delete_all() - { - //searching for contact entries - $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)'); - - if (!empty($dn_list)) { - foreach ($dn_list as $idx => $entry) { - $dn_list[$idx] = self::dn_encode($entry['dn']); - } - $this->delete($dn_list); - } - } - - - /** - * Execute the LDAP search based on the stored credentials - */ - private function _exec_search($count = false) - { - if ($this->ready) - { - $filter = $this->filter ? $this->filter : '(objectclass=*)'; - $function = $this->_scope2func($this->prop['scope'], $ns_function); - - $this->_debug("C: Search [$filter][dn: $this->base_dn]"); - - // when using VLV, we get the total count by... - if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) { - // ...either reading numSubOrdinates attribute - if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) { - $counts = ldap_get_entries($this->conn, $result_count); - for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++) - $this->vlv_count += $counts[$j]['numsubordinates'][0]; - $this->_debug("D: total numsubordinates = " . $this->vlv_count); - } - else if (!function_exists('ldap_parse_virtuallist_control')) // ...or by fetching all records dn and count them - $this->vlv_count = $this->_exec_search(true); - - $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size); - } - - // only fetch dn for count (should keep the payload low) - $attrs = $count ? array('dn') : array_values($this->fieldmap); - if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter, - $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']) - ) { - // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result - if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) { - if (ldap_parse_result($this->conn, $this->ldap_result, - $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) - ) { - ldap_parse_virtuallist_control($this->conn, $serverctrls, - $last_offset, $this->vlv_count, $vresult); - $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count"); - } - else { - $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn))); - } - } - - $entries_count = ldap_count_entries($this->conn, $this->ldap_result); - $this->_debug("S: $entries_count record(s)"); - - return $count ? $entries_count : true; - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - } - - return false; - } - - /** - * Choose the right PHP function according to scope property - */ - private function _scope2func($scope, &$ns_function = null) - { - switch ($scope) { - case 'sub': - $function = $ns_function = 'ldap_search'; - break; - case 'base': - $function = $ns_function = 'ldap_read'; - break; - default: - $function = 'ldap_list'; - $ns_function = 'ldap_read'; - break; - } - - return $function; - } - - /** - * Set server controls for Virtual List View (paginated listing) - */ - private function _vlv_set_controls($prop, $list_page, $page_size, $search = null) - { - $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort'])); - $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true); - - $sort = (array)$prop['sort']; - $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);" - . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)"); - - if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) { - $this->_debug("S: ".ldap_error($this->conn)); - $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported'); - return false; - } - - return true; - } - - - /** - * Converts LDAP entry into an array - */ - private function _ldap2result($rec) - { - $out = array(); - - if ($rec['dn']) - $out[$this->primary_key] = self::dn_encode($rec['dn']); - - foreach ($this->fieldmap as $rf => $lf) - { - for ($i=0; $i < $rec[$lf]['count']; $i++) { - if (!($value = $rec[$lf][$i])) - continue; - - list($col, $subtype) = explode(':', $rf); - $out['_raw_attrib'][$lf][$i] = $value; - - if ($col == 'email' && $this->mail_domain && !strpos($value, '@')) - $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain); - else if (in_array($col, array('street','zipcode','locality','country','region'))) - $out['address'.($subtype?':':'').$subtype][$i][$col] = $value; - else if ($col == 'address' && strpos($value, '$') !== false) // address data is represented as string separated with $ - list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $value); - else if ($rec[$lf]['count'] > 1) - $out[$rf][] = $value; - else - $out[$rf] = $value; - } - - // Make sure name fields aren't arrays (#1488108) - if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) { - $out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0]; - } - } - - return $out; - } - - - /** - * Return LDAP attribute(s) for the given field - */ - private function _map_field($field) - { - return (array)$this->coltypes[$field]['attributes']; - } - - - /** - * Convert a record data set into LDAP field attributes - */ - private function _map_data($save_cols) - { - // flatten composite fields first - foreach ($this->coltypes as $col => $colprop) { - if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) { - foreach ($values as $subtype => $childs) { - $subtype = $subtype ? ':'.$subtype : ''; - foreach ($childs as $i => $child_values) { - foreach ((array)$child_values as $childcol => $value) { - $save_cols[$childcol.$subtype][$i] = $value; - } - } - } - } - - // if addresses are to be saved as serialized string, do so - if (is_array($colprop['serialized'])) { - foreach ($colprop['serialized'] as $subtype => $delim) { - $key = $col.':'.$subtype; - foreach ((array)$save_cols[$key] as $i => $val) - $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country'])); - } - } - } - - $ldap_data = array(); - foreach ($this->fieldmap as $rf => $fld) { - $val = $save_cols[$rf]; - - // check for value in base field (eg.g email instead of email:foo) - list($col, $subtype) = explode(':', $rf); - if (!$val && !empty($save_cols[$col])) { - $val = $save_cols[$col]; - unset($save_cols[$col]); // only use this value once - } - else if (!$val && !$subtype) { // extract values from subtype cols - $val = $this->get_col_values($col, $save_cols, true); - } - - if (is_array($val)) - $val = array_filter($val); // remove empty entries - if ($fld && $val) { - // The field does exist, add it to the entry. - $ldap_data[$fld] = $val; - } - } - - return $ldap_data; - } - - - /** - * Returns unified attribute name (resolving aliases) - */ - private static function _attr_name($namev) - { - // list of known attribute aliases - static $aliases = array( - 'gn' => 'givenname', - 'rfc822mailbox' => 'email', - 'userid' => 'uid', - 'emailaddress' => 'email', - 'pkcs9email' => 'email', - ); - - list($name, $limit) = explode(':', $namev, 2); - $suffix = $limit ? ':'.$limit : ''; - - return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix; - } - - - /** - * Prints debug info to the log - */ - private function _debug($str) - { - if ($this->debug) { - rcube::write_log('ldap', $str); - } - } - - - /** - * Activate/deactivate debug mode - * - * @param boolean $dbg True if LDAP commands should be logged - * @access public - */ - function set_debug($dbg = true) - { - $this->debug = $dbg; - } - - - /** - * Quotes attribute value string - * - * @param string $str Attribute value - * @param bool $dn True if the attribute is a DN - * - * @return string Quoted string - */ - private static function _quote_string($str, $dn=false) - { - // take firt entry if array given - if (is_array($str)) - $str = reset($str); - - if ($dn) - $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c', - '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23'); - else - $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c', - '/'=>'\2f'); - - return strtr($str, $replace); - } - - - /** - * Setter for the current group - * (empty, has to be re-implemented by extending class) - */ - function set_group($group_id) - { - if ($group_id) - { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - $this->group_id = $group_id; - $this->group_data = $group_cache[$group_id]; - } - else - { - $this->group_id = 0; - $this->group_data = null; - } - } - - /** - * List all active contact groups of this source - * - * @param string Optional search string to match group name - * @return array Indexed list of contact groups, each a hash array - */ - function list_groups($search = null) - { - if (!$this->groups) - return array(); - - // use cached list for searching - $this->cache->expunge(); - if (!$search || ($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - $groups = array(); - if ($search) { - $search = mb_strtolower($search); - foreach ($group_cache as $group) { - if (strpos(mb_strtolower($group['name']), $search) !== false) - $groups[] = $group; - } - } - else - $groups = $group_cache; - - return array_values($groups); - } - - /** - * Fetch groups from server - */ - private function _fetch_groups($vlv_page = 0) - { - $base_dn = $this->groups_base_dn; - $filter = $this->prop['groups']['filter']; - $name_attr = $this->prop['groups']['name_attr']; - $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail'; - $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr); - $sort_attr = $sort_attrs[0]; - - $this->_debug("C: Search [$filter][dn: $base_dn]"); - - // use vlv to list groups - if ($this->prop['groups']['vlv']) { - $page_size = 200; - if (!$this->prop['groups']['sort']) - $this->prop['groups']['sort'] = $sort_attrs; - $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size); - } - - $function = $this->_scope2func($this->prop['groups']['scope'], $ns_function); - $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr))); - if ($res === false) - { - $this->_debug("S: ".ldap_error($this->conn)); - return array(); - } - - $ldap_data = ldap_get_entries($this->conn, $res); - $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)"); - - $groups = array(); - $group_sortnames = array(); - $group_count = $ldap_data["count"]; - for ($i=0; $i < $group_count; $i++) - { - $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr]; - $group_id = self::dn_encode($group_name); - $groups[$group_id]['ID'] = $group_id; - $groups[$group_id]['dn'] = $ldap_data[$i]['dn']; - $groups[$group_id]['name'] = $group_name; - $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']); - - // list email attributes of a group - for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) { - if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0) - $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j]; - } - - $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]); - } - - // recursive call can exit here - if ($vlv_page > 0) - return $groups; - - // call recursively until we have fetched all groups - while ($vlv_active && $group_count == $page_size) - { - $next_page = $this->_fetch_groups(++$vlv_page); - $groups = array_merge($groups, $next_page); - $group_count = count($next_page); - } - - // when using VLV the list of groups is already sorted - if (!$this->prop['groups']['vlv']) - array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups); - - // cache this - $this->cache->set('groups', $groups); - - return $groups; - } - - /** - * Get group properties such as name and email address(es) - * - * @param string Group identifier - * @return array Group properties as hash array - */ - function get_group($group_id) - { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - $group_data = $group_cache[$group_id]; - unset($group_data['dn'], $group_data['member_attr']); - - return $group_data; - } - - /** - * Create a contact group with the given name - * - * @param string The group name - * @return mixed False on error, array with record props in success - */ - function create_group($group_name) - { - $base_dn = $this->groups_base_dn; - $new_dn = "cn=$group_name,$base_dn"; - $new_gid = self::dn_encode($group_name); - $member_attr = $this->get_group_member_attr(); - $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; - - $new_entry = array( - 'objectClass' => $this->prop['groups']['object_classes'], - $name_attr => $group_name, - $member_attr => '', - ); - - if (!$this->ldap_add($new_dn, $new_entry)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - - $this->cache->remove('groups'); - - return array('id' => $new_gid, 'name' => $group_name); - } - - /** - * Delete the given group and all linked group members - * - * @param string Group identifier - * @return boolean True on success, false if no data was changed - */ - function delete_group($group_id) - { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - $base_dn = $this->groups_base_dn; - $group_name = $group_cache[$group_id]['name']; - $del_dn = "cn=$group_name,$base_dn"; - - if (!$this->ldap_delete($del_dn)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - - $this->cache->remove('groups'); - - return true; - } - - /** - * Rename a specific contact group - * - * @param string Group identifier - * @param string New name to set for this group - * @param string New group identifier (if changed, otherwise don't set) - * @return boolean New name on success, false if no data was changed - */ - function rename_group($group_id, $new_name, &$new_gid) - { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - $base_dn = $this->groups_base_dn; - $group_name = $group_cache[$group_id]['name']; - $old_dn = "cn=$group_name,$base_dn"; - $new_rdn = "cn=$new_name"; - $new_gid = self::dn_encode($new_name); - - if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return false; - } - - $this->cache->remove('groups'); - - return $new_name; - } - - /** - * Add the given contact records the a certain group - * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added - */ - function add_to_group($group_id, $contact_ids) - { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - if (!is_array($contact_ids)) - $contact_ids = explode(',', $contact_ids); - - $base_dn = $this->groups_base_dn; - $group_name = $group_cache[$group_id]['name']; - $member_attr = $group_cache[$group_id]['member_attr']; - $group_dn = "cn=$group_name,$base_dn"; - - $new_attrs = array(); - foreach ($contact_ids as $id) - $new_attrs[$member_attr][] = self::dn_decode($id); - - if (!$this->ldap_mod_add($group_dn, $new_attrs)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return 0; - } - - $this->cache->remove('groups'); - - return count($new_attrs['member']); - } - - /** - * Remove the given contact records from a certain group - * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members - */ - function remove_from_group($group_id, $contact_ids) - { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - $base_dn = $this->groups_base_dn; - $group_name = $group_cache[$group_id]['name']; - $member_attr = $group_cache[$group_id]['member_attr']; - $group_dn = "cn=$group_name,$base_dn"; - - $del_attrs = array(); - foreach (explode(",", $contact_ids) as $id) - $del_attrs[$member_attr][] = self::dn_decode($id); - - if (!$this->ldap_mod_del($group_dn, $del_attrs)) { - $this->set_error(self::ERROR_SAVING, 'errorsaving'); - return 0; - } - - $this->cache->remove('groups'); - - return count($del_attrs['member']); - } - - /** - * Get group assignments of a specific contact record - * - * @param mixed Record identifier - * - * @return array List of assigned groups as ID=>Name pairs - * @since 0.5-beta - */ - function get_record_groups($contact_id) - { - if (!$this->groups) - return array(); - - $base_dn = $this->groups_base_dn; - $contact_dn = self::dn_decode($contact_id); - $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; - $member_attr = $this->get_group_member_attr(); - $add_filter = ''; - if ($member_attr != 'member' && $member_attr != 'uniqueMember') - $add_filter = "($member_attr=$contact_dn)"; - $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\')); - - $this->_debug("C: Search [$filter][dn: $base_dn]"); - - $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr)); - if ($res === false) - { - $this->_debug("S: ".ldap_error($this->conn)); - return array(); - } - $ldap_data = ldap_get_entries($this->conn, $res); - $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)"); - - $groups = array(); - for ($i=0; $i<$ldap_data["count"]; $i++) - { - $group_name = $ldap_data[$i][$name_attr][0]; - $group_id = self::dn_encode($group_name); - $groups[$group_id] = $group_id; - } - return $groups; - } - - /** - * Detects group member attribute name - */ - private function get_group_member_attr($object_classes = array()) - { - if (empty($object_classes)) { - $object_classes = $this->prop['groups']['object_classes']; - } - if (!empty($object_classes)) { - foreach ((array)$object_classes as $oc) { - switch (strtolower($oc)) { - case 'group': - case 'groupofnames': - case 'kolabgroupofnames': - $member_attr = 'member'; - break; - - case 'groupofuniquenames': - case 'kolabgroupofuniquenames': - $member_attr = 'uniqueMember'; - break; - } - } - } - - if (!empty($member_attr)) { - return $member_attr; - } - - if (!empty($this->prop['groups']['member_attr'])) { - return $this->prop['groups']['member_attr']; - } - - return 'member'; - } - - - /** - * Generate BER encoded string for Virtual List View option - * - * @param integer List offset (first record) - * @param integer Records per page - * @return string BER encoded option value - */ - private function _vlv_ber_encode($offset, $rpp, $search = '') - { - # this string is ber-encoded, php will prefix this value with: - # 04 (octet string) and 10 (length of 16 bytes) - # the code behind this string is broken down as follows: - # 30 = ber sequence with a length of 0e (14) bytes following - # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0) - # 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24) - # a0 = type context-specific/constructed with a length of 06 (6) bytes following - # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1) - # 02 = type integer with 2 bytes following (contentCount): 01 00 - - # whith a search string present: - # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here) - # 81 indicates a user string is present where as a a0 indicates just a offset search - # 81 = type context-specific/constructed with a length of 06 (6) bytes following - - # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the - # encoding of integer values (note: these values are in - # two-complement form so since offset will never be negative bit 8 of the - # leftmost octet should never by set to 1): - # 8.3.2: If the contents octets of an integer value encoding consist - # of more than one octet, then the bits of the first octet (rightmost) and bit 8 - # of the second (to the left of first octet) octet: - # a) shall not all be ones; and - # b) shall not all be zero - - if ($search) - { - $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search); - $ber_val = self::_string2hex($search); - $str = self::_ber_addseq($ber_val, '81'); - } - else - { - # construct the string from right to left - $str = "020100"; # contentCount - - $ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format - - // calculate octet length of $ber_val - $str = self::_ber_addseq($ber_val, '02') . $str; - - // now compute length over $str - $str = self::_ber_addseq($str, 'a0'); - } - - // now tack on records per page - $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str; - - // now tack on sequence identifier and length - $str = self::_ber_addseq($str, '30'); - - return pack('H'.strlen($str), $str); - } - - - /** - * create ber encoding for sort control - * - * @param array List of cols to sort by - * @return string BER encoded option value - */ - private function _sort_ber_encode($sortcols) - { - $str = ''; - foreach (array_reverse((array)$sortcols) as $col) { - $ber_val = self::_string2hex($col); - - # 30 = ber sequence with a length of octet value - # 04 = octet string with a length of the ascii value - $oct = self::_ber_addseq($ber_val, '04'); - $str = self::_ber_addseq($oct, '30') . $str; - } - - // now tack on sequence identifier and length - $str = self::_ber_addseq($str, '30'); - - return pack('H'.strlen($str), $str); - } - - /** - * Add BER sequence with correct length and the given identifier - */ - private static function _ber_addseq($str, $identifier) - { - $len = dechex(strlen($str)/2); - if (strlen($len) % 2 != 0) - $len = '0'.$len; - - return $identifier . $len . $str; - } - - /** - * Returns BER encoded integer value in hex format - */ - private static function _ber_encode_int($offset) - { - $val = dechex($offset); - $prefix = ''; - - // check if bit 8 of high byte is 1 - if (preg_match('/^[89abcdef]/', $val)) - $prefix = '00'; - - if (strlen($val)%2 != 0) - $prefix .= '0'; - - return $prefix . $val; - } - - /** - * Returns ascii string encoded in hex - */ - private static function _string2hex($str) - { - $hex = ''; - for ($i=0; $i < strlen($str); $i++) - $hex .= dechex(ord($str[$i])); - return $hex; - } - - /** - * HTML-safe DN string encoding - * - * @param string $str DN string - * - * @return string Encoded HTML identifier string - */ - static function dn_encode($str) - { - // @TODO: to make output string shorter we could probably - // remove dc=* items from it - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); - } - - /** - * Decodes DN string encoded with _dn_encode() - * - * @param string $str Encoded HTML identifier string - * - * @return string DN string - */ - static function dn_decode($str) - { - $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT); - return base64_decode($str); - } - - /** - * Wrapper for ldap_add() - */ - protected function ldap_add($dn, $entry) - { - $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true)); - - $res = ldap_add($this->conn, $dn, $entry); - if ($res === false) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; - } - - /** - * Wrapper for ldap_delete() - */ - protected function ldap_delete($dn) - { - $this->_debug("C: Delete [dn: $dn]"); - - $res = ldap_delete($this->conn, $dn); - if ($res === false) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; - } - - /** - * Wrapper for ldap_mod_replace() - */ - protected function ldap_mod_replace($dn, $entry) - { - $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true)); - - if (!ldap_mod_replace($this->conn, $dn, $entry)) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; - } - - /** - * Wrapper for ldap_mod_add() - */ - protected function ldap_mod_add($dn, $entry) - { - $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true)); - - if (!ldap_mod_add($this->conn, $dn, $entry)) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; - } - - /** - * Wrapper for ldap_mod_del() - */ - protected function ldap_mod_del($dn, $entry) - { - $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true)); - - if (!ldap_mod_del($this->conn, $dn, $entry)) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; - } - - /** - * Wrapper for ldap_rename() - */ - protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true) - { - $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]"); - - if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; - } - - /** - * Wrapper for ldap_list() - */ - protected function ldap_list($dn, $filter, $attrs = array('')) - { - $list = array(); - $this->_debug("C: List [dn: $dn] [{$filter}]"); - - if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) { - $list = ldap_get_entries($this->conn, $result); - - if ($list === false) { - $this->_debug("S: ".ldap_error($this->conn)); - return array(); - } - - $count = $list['count']; - unset($list['count']); - - $this->_debug("S: $count record(s)"); - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - - return $list; - } - -} diff --git a/program/include/rcube_message.php b/program/include/rcube_message.php deleted file mode 100644 index 38d18171c..000000000 --- a/program/include/rcube_message.php +++ /dev/null @@ -1,760 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_message.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2010, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Logical representation of a mail message with all its data | - | and related functions | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Logical representation of a mail message with all its data - * and related functions - * - * @package Mail - * @author Thomas Bruederli <roundcube@gmail.com> - */ -class rcube_message -{ - /** - * Instace of framework class. - * - * @var rcube - */ - private $app; - - /** - * Instance of storage class - * - * @var rcube_storage - */ - private $storage; - - /** - * Instance of mime class - * - * @var rcube_mime - */ - private $mime; - private $opt = array(); - private $parse_alternative = false; - - public $uid; - public $folder; - public $headers; - public $parts = array(); - public $mime_parts = array(); - public $inline_parts = array(); - public $attachments = array(); - public $subject = ''; - public $sender = null; - public $is_safe = false; - - - /** - * __construct - * - * Provide a uid, and parse message structure. - * - * @param string $uid The message UID. - * @param string $folder Folder name - * - * @see self::$app, self::$storage, self::$opt, self::$parts - */ - function __construct($uid, $folder = null) - { - $this->uid = $uid; - $this->app = rcube::get_instance(); - $this->storage = $this->app->get_storage(); - $this->folder = strlen($folder) ? $folder : $this->storage->get_folder(); - $this->storage->set_options(array('all_headers' => true)); - - // Set current folder - $this->storage->set_folder($this->folder); - - $this->headers = $this->storage->get_message($uid); - - if (!$this->headers) - return; - - $this->mime = new rcube_mime($this->headers->charset); - - $this->subject = $this->mime->decode_mime_string($this->headers->subject); - list(, $this->sender) = each($this->mime->decode_address_list($this->headers->from, 1)); - - $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid])); - $this->opt = array( - 'safe' => $this->is_safe, - 'prefer_html' => $this->app->config->get('prefer_html'), - 'get_url' => $this->app->url(array( - 'action' => 'get', - 'mbox' => $this->storage->get_folder(), - 'uid' => $uid)) - ); - - if (!empty($this->headers->structure)) { - $this->get_mime_numbers($this->headers->structure); - $this->parse_structure($this->headers->structure); - } - else { - $this->body = $this->storage->get_body($uid); - } - - // notify plugins and let them analyze this structured message object - $this->app->plugins->exec_hook('message_load', array('object' => $this)); - } - - - /** - * Return a (decoded) message header - * - * @param string $name Header name - * @param bool $row Don't mime-decode the value - * @return string Header value - */ - public function get_header($name, $raw = false) - { - if (empty($this->headers)) - return null; - - if ($this->headers->$name) - $value = $this->headers->$name; - else if ($this->headers->others[$name]) - $value = $this->headers->others[$name]; - - return $raw ? $value : $this->mime->decode_header($value); - } - - - /** - * Set is_safe var and session data - * - * @param bool $safe enable/disable - */ - public function set_safe($safe = true) - { - $this->is_safe = $safe; - $_SESSION['safe_messages'][$this->uid] = $this->is_safe; - } - - - /** - * Compose a valid URL for getting a message part - * - * @param string $mime_id Part MIME-ID - * @return string URL or false if part does not exist - */ - public function get_part_url($mime_id, $embed = false) - { - if ($this->mime_parts[$mime_id]) - return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1' : ''); - else - return false; - } - - - /** - * Get content of a specific part of this message - * - * @param string $mime_id Part MIME-ID - * @param resource $fp File pointer to save the message part - * @param boolean $skip_charset_conv Disables charset conversion - * - * @return string Part content - */ - public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false) - { - if ($part = $this->mime_parts[$mime_id]) { - // stored in message structure (winmail/inline-uuencode) - if (!empty($part->body) || $part->encoding == 'stream') { - if ($fp) { - fwrite($fp, $part->body); - } - return $fp ? true : $part->body; - } - - // get from IMAP - $this->storage->set_folder($this->folder); - - return $this->storage->get_message_part($this->uid, $mime_id, $part, NULL, $fp, $skip_charset_conv); - } - } - - - /** - * Determine if the message contains a HTML part - * - * @param bool $recursive Enables checking in all levels of the structure - * - * @return bool True if a HTML is available, False if not - */ - function has_html_part($recursive = true) - { - // check all message parts - foreach ($this->parts as $part) { - if ($part->mimetype == 'text/html') { - // Level check, we'll skip e.g. HTML attachments - if (!$recursive) { - $level = explode('.', $part->mime_id); - - // Skip if level too deep or part has a file name - if (count($level) > 2 || $part->filename) { - continue; - } - - // HTML part can be on the lower level, if not... - if (count($level) > 1) { - array_pop($level); - $parent = $this->mime_parts[join('.', $level)]; - // ... parent isn't multipart/alternative or related - if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') { - continue; - } - } - } - - return true; - } - } - - return false; - } - - - /** - * Return the first HTML part of this message - * - * @return string HTML message part content - */ - function first_html_part() - { - // check all message parts - foreach ($this->mime_parts as $pid => $part) { - if ($part->mimetype == 'text/html') { - return $this->get_part_content($pid); - } - } - } - - - /** - * Return the first text part of this message - * - * @param rcube_message_part $part Reference to the part if found - * @return string Plain text message/part content - */ - function first_text_part(&$part=null) - { - // no message structure, return complete body - if (empty($this->parts)) - return $this->body; - - // check all message parts - foreach ($this->mime_parts as $mime_id => $part) { - if ($part->mimetype == 'text/plain') { - return $this->get_part_content($mime_id); - } - else if ($part->mimetype == 'text/html') { - $out = $this->get_part_content($mime_id); - - // remove special chars encoding - $trans = array_flip(get_html_translation_table(HTML_ENTITIES)); - $out = strtr($out, $trans); - - // create instance of html2text class - $txt = new html2text($out); - return $txt->get_text(); - } - } - - $part = null; - return null; - } - - - /** - * Checks if part of the message is an attachment (or part of it) - * - * @param rcube_message_part $part Message part - * - * @return bool True if the part is an attachment part - */ - public function is_attachment($part) - { - foreach ($this->attachments as $att_part) { - if ($att_part->mime_id == $part->mime_id) { - return true; - } - - // check if the part is a subpart of another attachment part (message/rfc822) - if ($att_part->mimetype == 'message/rfc822') { - if (in_array($part, (array)$att_part->parts)) { - return true; - } - } - } - - return false; - } - - - /** - * Read the message structure returend by the IMAP server - * and build flat lists of content parts and attachments - * - * @param rcube_message_part $structure Message structure node - * @param bool $recursive True when called recursively - */ - private function parse_structure($structure, $recursive = false) - { - // real content-type of message/rfc822 part - if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) - $mimetype = $structure->real_mimetype; - else - $mimetype = $structure->mimetype; - - // show message headers - if ($recursive && is_array($structure->headers) && isset($structure->headers['subject'])) { - $c = new stdClass; - $c->type = 'headers'; - $c->headers = &$structure->headers; - $this->parts[] = $c; - } - - // Allow plugins to handle message parts - $plugin = $this->app->plugins->exec_hook('message_part_structure', - array('object' => $this, 'structure' => $structure, - 'mimetype' => $mimetype, 'recursive' => $recursive)); - - if ($plugin['abort']) - return; - - $structure = $plugin['structure']; - list($message_ctype_primary, $message_ctype_secondary) = explode('/', $plugin['mimetype']); - - // print body if message doesn't have multiple parts - if ($message_ctype_primary == 'text' && !$recursive) { - $structure->type = 'content'; - $this->parts[] = &$structure; - - // Parse simple (plain text) message body - if ($message_ctype_secondary == 'plain') - foreach ((array)$this->uu_decode($structure) as $uupart) { - $this->mime_parts[$uupart->mime_id] = $uupart; - $this->attachments[] = $uupart; - } - } - // the same for pgp signed messages - else if ($mimetype == 'application/pgp' && !$recursive) { - $structure->type = 'content'; - $this->parts[] = &$structure; - } - // message contains (more than one!) alternative parts - else if ($mimetype == 'multipart/alternative' - && is_array($structure->parts) && count($structure->parts) > 1 - ) { - // get html/plaintext parts - $plain_part = $html_part = $print_part = $related_part = null; - - foreach ($structure->parts as $p => $sub_part) { - $sub_mimetype = $sub_part->mimetype; - - // skip empty text parts - if (!$sub_part->size && preg_match('#^text/(plain|html|enriched)$#', $sub_mimetype)) { - continue; - } - - // check if sub part is - if ($sub_mimetype == 'text/plain') - $plain_part = $p; - else if ($sub_mimetype == 'text/html') - $html_part = $p; - else if ($sub_mimetype == 'text/enriched') - $enriched_part = $p; - else if (in_array($sub_mimetype, array('multipart/related', 'multipart/mixed', 'multipart/alternative'))) - $related_part = $p; - } - - // parse related part (alternative part could be in here) - if ($related_part !== null && !$this->parse_alternative) { - $this->parse_alternative = true; - $this->parse_structure($structure->parts[$related_part], true); - $this->parse_alternative = false; - - // if plain part was found, we should unset it if html is preferred - if ($this->opt['prefer_html'] && count($this->parts)) - $plain_part = null; - } - - // choose html/plain part to print - if ($html_part !== null && $this->opt['prefer_html']) { - $print_part = &$structure->parts[$html_part]; - } - else if ($enriched_part !== null) { - $print_part = &$structure->parts[$enriched_part]; - } - else if ($plain_part !== null) { - $print_part = &$structure->parts[$plain_part]; - } - - // add the right message body - if (is_object($print_part)) { - $print_part->type = 'content'; - $this->parts[] = $print_part; - } - // show plaintext warning - else if ($html_part !== null && empty($this->parts)) { - $c = new stdClass; - $c->type = 'content'; - $c->ctype_primary = 'text'; - $c->ctype_secondary = 'plain'; - $c->mimetype = 'text/plain'; - $c->realtype = 'text/html'; - - $this->parts[] = $c; - } - - // add html part as attachment - if ($html_part !== null && $structure->parts[$html_part] !== $print_part) { - $html_part = &$structure->parts[$html_part]; - $html_part->mimetype = 'text/html'; - - $this->attachments[] = $html_part; - } - } - // this is an ecrypted message -> create a plaintext body with the according message - else if ($mimetype == 'multipart/encrypted') { - $p = new stdClass; - $p->type = 'content'; - $p->ctype_primary = 'text'; - $p->ctype_secondary = 'plain'; - $p->mimetype = 'text/plain'; - $p->realtype = 'multipart/encrypted'; - - $this->parts[] = $p; - } - // message contains multiple parts - else if (is_array($structure->parts) && !empty($structure->parts)) { - // iterate over parts - for ($i=0; $i < count($structure->parts); $i++) { - $mail_part = &$structure->parts[$i]; - $primary_type = $mail_part->ctype_primary; - $secondary_type = $mail_part->ctype_secondary; - - // real content-type of message/rfc822 - if ($mail_part->real_mimetype) { - $part_orig_mimetype = $mail_part->mimetype; - $part_mimetype = $mail_part->real_mimetype; - list($primary_type, $secondary_type) = explode('/', $part_mimetype); - } - else - $part_mimetype = $mail_part->mimetype; - - // multipart/alternative - if ($primary_type == 'multipart') { - $this->parse_structure($mail_part, true); - - // list message/rfc822 as attachment as well (mostly .eml) - if ($part_orig_mimetype == 'message/rfc822' && !empty($mail_part->filename)) - $this->attachments[] = $mail_part; - } - // part text/[plain|html] or delivery status - else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') || - in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification')) - ) { - // Allow plugins to handle also this part - $plugin = $this->app->plugins->exec_hook('message_part_structure', - array('object' => $this, 'structure' => $mail_part, - 'mimetype' => $part_mimetype, 'recursive' => true)); - - if ($plugin['abort']) - continue; - - if ($part_mimetype == 'text/html') { - $got_html_part = true; - } - - $mail_part = $plugin['structure']; - list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']); - - // add text part if it matches the prefs - if (!$this->parse_alternative || - ($secondary_type == 'html' && $this->opt['prefer_html']) || - ($secondary_type == 'plain' && !$this->opt['prefer_html']) - ) { - $mail_part->type = 'content'; - $this->parts[] = $mail_part; - } - - // list as attachment as well - if (!empty($mail_part->filename)) { - $this->attachments[] = $mail_part; - } - // list html part as attachment (here the part is most likely inside a multipart/related part) - else if ($this->parse_alternative && ($secondary_type == 'html' && !$this->opt['prefer_html'])) { - $this->attachments[] = $mail_part; - } - } - // part message/* - else if ($primary_type == 'message') { - $this->parse_structure($mail_part, true); - - // list as attachment as well (mostly .eml) - if (!empty($mail_part->filename)) - $this->attachments[] = $mail_part; - } - // ignore "virtual" protocol parts - else if ($primary_type == 'protocol') { - continue; - } - // part is Microsoft Outlook TNEF (winmail.dat) - else if ($part_mimetype == 'application/ms-tnef') { - foreach ((array)$this->tnef_decode($mail_part) as $tpart) { - $this->mime_parts[$tpart->mime_id] = $tpart; - $this->attachments[] = $tpart; - } - } - // part is a file/attachment - else if (preg_match('/^(inline|attach)/', $mail_part->disposition) || - $mail_part->headers['content-id'] || - ($mail_part->filename && - (empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition))) - ) { - // skip apple resource forks - if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile') - continue; - - // part belongs to a related message and is linked - if ($mimetype == 'multipart/related' - && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])) { - if ($mail_part->headers['content-id']) - $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']); - if ($mail_part->headers['content-location']) - $mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location']; - - $this->inline_parts[] = $mail_part; - } - // attachment encapsulated within message/rfc822 part needs further decoding (#1486743) - else if ($part_orig_mimetype == 'message/rfc822') { - $this->parse_structure($mail_part, true); - - // list as attachment as well (mostly .eml) - if (!empty($mail_part->filename)) - $this->attachments[] = $mail_part; - } - // regular attachment with valid content type - // (content-type name regexp according to RFC4288.4.2) - else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) { - if (!$mail_part->filename) - $mail_part->filename = 'Part '.$mail_part->mime_id; - - $this->attachments[] = $mail_part; - } - // attachment with invalid content type - // replace malformed content type with application/octet-stream (#1487767) - else if ($mail_part->filename) { - $mail_part->ctype_primary = 'application'; - $mail_part->ctype_secondary = 'octet-stream'; - $mail_part->mimetype = 'application/octet-stream'; - - $this->attachments[] = $mail_part; - } - } - // attachment part as message/rfc822 (#1488026) - else if ($mail_part->mimetype == 'message/rfc822') { - $this->parse_structure($mail_part); - } - } - - // if this was a related part try to resolve references - if ($mimetype == 'multipart/related' && sizeof($this->inline_parts)) { - $a_replaces = array(); - $img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/'; - - foreach ($this->inline_parts as $inline_object) { - $part_url = $this->get_part_url($inline_object->mime_id, true); - if ($inline_object->content_id) - $a_replaces['cid:'.$inline_object->content_id] = $part_url; - if ($inline_object->content_location) { - $a_replaces[$inline_object->content_location] = $part_url; - } - - if (!empty($inline_object->filename)) { - // MS Outlook sends sometimes non-related attachments as related - // In this case multipart/related message has only one text part - // We'll add all such attachments to the attachments list - if (!isset($got_html_part) && empty($inline_object->content_id)) { - $this->attachments[] = $inline_object; - } - // MS Outlook sometimes also adds non-image attachments as related - // We'll add all such attachments to the attachments list - // Warning: some browsers support pdf in <img/> - else if (!preg_match($img_regexp, $inline_object->mimetype)) { - $this->attachments[] = $inline_object; - } - // @TODO: we should fetch HTML body and find attachment's content-id - // to handle also image attachments without reference in the body - // @TODO: should we list all image attachments in text mode? - } - } - - // add replace array to each content part - // (will be applied later when part body is available) - foreach ($this->parts as $i => $part) { - if ($part->type == 'content') - $this->parts[$i]->replaces = $a_replaces; - } - } - } - // message is a single part non-text - else if ($structure->filename) { - $this->attachments[] = $structure; - } - // message is a single part non-text (without filename) - else if (preg_match('/application\//i', $mimetype)) { - $structure->filename = 'Part '.$structure->mime_id; - $this->attachments[] = $structure; - } - } - - - /** - * Fill aflat array with references to all parts, indexed by part numbers - * - * @param rcube_message_part $part Message body structure - */ - private function get_mime_numbers(&$part) - { - if (strlen($part->mime_id)) - $this->mime_parts[$part->mime_id] = &$part; - - if (is_array($part->parts)) - for ($i=0; $i<count($part->parts); $i++) - $this->get_mime_numbers($part->parts[$i]); - } - - - /** - * Decode a Microsoft Outlook TNEF part (winmail.dat) - * - * @param rcube_message_part $part Message part to decode - * @return array - */ - function tnef_decode(&$part) - { - // @TODO: attachment may be huge, hadle it via file - if (!isset($part->body)) { - $this->storage->set_folder($this->folder); - $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part); - } - - $parts = array(); - $tnef = new tnef_decoder; - $tnef_arr = $tnef->decompress($part->body); - - foreach ($tnef_arr as $pid => $winatt) { - $tpart = new rcube_message_part; - - $tpart->filename = trim($winatt['name']); - $tpart->encoding = 'stream'; - $tpart->ctype_primary = trim(strtolower($winatt['type'])); - $tpart->ctype_secondary = trim(strtolower($winatt['subtype'])); - $tpart->mimetype = $tpart->ctype_primary . '/' . $tpart->ctype_secondary; - $tpart->mime_id = 'winmail.' . $part->mime_id . '.' . $pid; - $tpart->size = $winatt['size']; - $tpart->body = $winatt['stream']; - - $parts[] = $tpart; - unset($tnef_arr[$pid]); - } - - return $parts; - } - - - /** - * Parse message body for UUencoded attachments bodies - * - * @param rcube_message_part $part Message part to decode - * @return array - */ - function uu_decode(&$part) - { - // @TODO: messages may be huge, hadle body via file - if (!isset($part->body)) { - $this->storage->set_folder($this->folder); - $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part); - } - - $parts = array(); - // FIXME: line length is max.65? - $uu_regexp = '/begin [0-7]{3,4} ([^\n]+)\n/s'; - - if (preg_match_all($uu_regexp, $part->body, $matches, PREG_SET_ORDER)) { - // update message content-type - $part->ctype_primary = 'multipart'; - $part->ctype_secondary = 'mixed'; - $part->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary; - $uu_endstring = "`\nend\n"; - - // add attachments to the structure - foreach ($matches as $pid => $att) { - $startpos = strpos($part->body, $att[1]) + strlen($att[1]) + 1; // "\n" - $endpos = strpos($part->body, $uu_endstring); - $filebody = substr($part->body, $startpos, $endpos-$startpos); - - // remove attachments bodies from the message body - $part->body = substr_replace($part->body, "", $startpos, $endpos+strlen($uu_endstring)-$startpos); - - $uupart = new rcube_message_part; - - $uupart->filename = trim($att[1]); - $uupart->encoding = 'stream'; - $uupart->body = convert_uudecode($filebody); - $uupart->size = strlen($uupart->body); - $uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid; - - $ctype = rcube_mime::content_type($uupart->body, $uupart->filename, 'application/octet-stream', true); - $uupart->mimetype = $ctype; - list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype); - - $parts[] = $uupart; - unset($matches[$pid]); - } - - // remove attachments bodies from the message body - $part->body = preg_replace($uu_regexp, '', $part->body); - } - - return $parts; - } - - - /** - * Deprecated methods (to be removed) - */ - - public static function unfold_flowed($text) - { - return rcube_mime::unfold_flowed($text); - } - - public static function format_flowed($text, $length = 72) - { - return rcube_mime::format_flowed($text, $length); - } - -} diff --git a/program/include/rcube_message_header.php b/program/include/rcube_message_header.php deleted file mode 100644 index 378fb98c9..000000000 --- a/program/include/rcube_message_header.php +++ /dev/null @@ -1,287 +0,0 @@ -<?php - -/** - +-----------------------------------------------------------------------+ - | program/include/rcube_message_header.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | E-mail message headers representation | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - -/** - * Struct representing an e-mail message header - * - * @package Mail - * @author Aleksander Machniak <alec@alec.pl> - */ -class rcube_message_header -{ - /** - * Message sequence number - * - * @var int - */ - public $id; - - /** - * Message unique identifier - * - * @var int - */ - public $uid; - - /** - * Message subject - * - * @var string - */ - public $subject; - - /** - * Message sender (From) - * - * @var string - */ - public $from; - - /** - * Message recipient (To) - * - * @var string - */ - public $to; - - /** - * Message additional recipients (Cc) - * - * @var string - */ - public $cc; - - /** - * Message Reply-To header - * - * @var string - */ - public $replyto; - - /** - * Message In-Reply-To header - * - * @var string - */ - public $in_reply_to; - - /** - * Message date (Date) - * - * @var string - */ - public $date; - - /** - * Message identifier (Message-ID) - * - * @var string - */ - public $messageID; - - /** - * Message size - * - * @var int - */ - public $size; - - /** - * Message encoding - * - * @var string - */ - public $encoding; - - /** - * Message charset - * - * @var string - */ - public $charset; - - /** - * Message Content-type - * - * @var string - */ - public $ctype; - - /** - * Message timestamp (based on message date) - * - * @var int - */ - public $timestamp; - - /** - * IMAP bodystructure string - * - * @var string - */ - public $bodystructure; - - /** - * IMAP internal date - * - * @var string - */ - public $internaldate; - - /** - * Message References header - * - * @var string - */ - public $references; - - /** - * Message priority (X-Priority) - * - * @var int - */ - public $priority; - - /** - * Message receipt recipient - * - * @var string - */ - public $mdn_to; - - /** - * Other message headers - * - * @var array - */ - public $others = array(); - - /** - * Message flags - * - * @var array - */ - public $flags = array(); - - // map header to rcube_message_header object property - private $obj_headers = array( - 'date' => 'date', - 'from' => 'from', - 'to' => 'to', - 'subject' => 'subject', - 'reply-to' => 'replyto', - 'cc' => 'cc', - 'bcc' => 'bcc', - 'content-transfer-encoding' => 'encoding', - 'in-reply-to' => 'in_reply_to', - 'content-type' => 'ctype', - 'references' => 'references', - 'return-receipt-to' => 'mdn_to', - 'disposition-notification-to' => 'mdn_to', - 'x-confirm-reading-to' => 'mdn_to', - 'message-id' => 'messageID', - 'x-priority' => 'priority', - ); - - /** - * Returns header value - */ - public function get($name, $decode = true) - { - $name = strtolower($name); - - if (isset($this->obj_headers[$name])) { - $value = $this->{$this->obj_headers[$name]}; - } - else { - $value = $this->others[$name]; - } - - return $decode ? rcube_mime::decode_header($value, $this->charset) : $value; - } - - /** - * Sets header value - */ - public function set($name, $value) - { - $name = strtolower($name); - - if (isset($this->obj_headers[$name])) { - $this->{$this->obj_headers[$name]} = $value; - } - else { - $this->others[$name] = $value; - } - } -} - - -/** - * Class for sorting an array of rcube_message_header objects in a predetermined order. - * - * @package Mail - * @author Aleksander Machniak <alec@alec.pl> - */ -class rcube_message_header_sorter -{ - private $uids = array(); - - - /** - * Set the predetermined sort order. - * - * @param array $index Numerically indexed array of IMAP UIDs - */ - function set_index($index) - { - $index = array_flip($index); - - $this->uids = $index; - } - - /** - * Sort the array of header objects - * - * @param array $headers Array of rcube_message_header objects indexed by UID - */ - function sort_headers(&$headers) - { - uksort($headers, array($this, "compare_uids")); - } - - /** - * Sort method called by uksort() - * - * @param int $a Array key (UID) - * @param int $b Array key (UID) - */ - function compare_uids($a, $b) - { - // then find each sequence number in my ordered list - $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1; - $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1; - - // return the relative position as the comparison value - return $posa - $posb; - } -} diff --git a/program/include/rcube_message_part.php b/program/include/rcube_message_part.php deleted file mode 100644 index 80a019e50..000000000 --- a/program/include/rcube_message_part.php +++ /dev/null @@ -1,100 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_message_part.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Class representing a message part | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Class representing a message part - * - * @package Mail - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - * @version 2.0 - */ -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'; - - /** - * Complete content type - * - * @var string - */ - public $mimetype = 'text/plain'; - - public $disposition = ''; - public $filename = ''; - public $encoding = '8bit'; - public $charset = ''; - - /** - * Part size in bytes - * - * @var int - */ - public $size = 0; - - /** - * Part headers - * - * @var array - */ - public $headers = array(); - - public $d_parameters = array(); - public $ctype_parameters = array(); - - - /** - * Clone handler. - */ - function __clone() - { - if (isset($this->parts)) { - foreach ($this->parts as $idx => $part) { - if (is_object($part)) { - $this->parts[$idx] = clone $part; - } - } - } - } - -} diff --git a/program/include/rcube_mime.php b/program/include/rcube_mime.php deleted file mode 100644 index d8e04a97c..000000000 --- a/program/include/rcube_mime.php +++ /dev/null @@ -1,704 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_mime.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | MIME message parsing utilities | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Class for parsing MIME messages - * - * @package Mail - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - */ -class rcube_mime -{ - private static $default_charset; - - - /** - * Object constructor. - */ - function __construct($default_charset = null) - { - self::$default_charset = $default_charset; - } - - - /** - * Returns message/object character set name - * - * @return string Characted set name - */ - public static function get_charset() - { - if (self::$default_charset) { - return self::$default_charset; - } - - if ($charset = rcube::get_instance()->config->get('default_charset')) { - return $charset; - } - - return RCMAIL_CHARSET; - } - - - /** - * Parse the given raw message source and return a structure - * of rcube_message_part objects. - * - * It makes use of the PEAR:Mail_mimeDecode library - * - * @param string The message source - * @return object rcube_message_part The message structure - */ - public static function parse_message($raw_body) - { - $mime = new Mail_mimeDecode($raw_body); - $struct = $mime->decode(array('include_bodies' => true, 'decode_bodies' => true)); - return self::structure_part($struct); - } - - - /** - * Recursive method to convert a Mail_mimeDecode part into a rcube_message_part object - * - * @param object A message part struct - * @param int Part count - * @param string Parent MIME ID - * - * @return object rcube_message_part - */ - private static function structure_part($part, $count=0, $parent='') - { - $struct = new rcube_message_part; - $struct->mime_id = $part->mime_id ? $part->mime_id : (empty($parent) ? (string)$count : "$parent.$count"); - $struct->headers = $part->headers; - $struct->ctype_primary = $part->ctype_primary; - $struct->ctype_secondary = $part->ctype_secondary; - $struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary; - $struct->ctype_parameters = $part->ctype_parameters; - - if ($part->headers['content-transfer-encoding']) - $struct->encoding = $part->headers['content-transfer-encoding']; - if ($part->ctype_parameters['charset']) - $struct->charset = $part->ctype_parameters['charset']; - - $part_charset = $struct->charset ? $struct->charset : self::get_charset(); - - // determine filename - if (($filename = $part->d_parameters['filename']) || ($filename = $part->ctype_parameters['name'])) { - $struct->filename = rcube_mime::decode_mime_string($filename, $part_charset); - } - - // copy part body and convert it to UTF-8 if necessary - $struct->body = $part->ctype_primary == 'text' || !$part->ctype_parameters['charset'] ? rcube_charset::convert($part->body, $part_charset) : $part->body; - $struct->size = strlen($part->body); - $struct->disposition = $part->disposition; - - foreach ((array)$part->parts as $child_part) { - $struct->parts[] = self::structure_part($child_part, ++$count, $struct->mime_id); - } - - return $struct; - } - - - /** - * Split an address list into a structured array list - * - * @param string $input Input string - * @param int $max List only this number of addresses - * @param boolean $decode Decode address strings - * @param string $fallback Fallback charset if none specified - * - * @return array Indexed list of addresses - */ - static function decode_address_list($input, $max = null, $decode = true, $fallback = null) - { - $a = self::parse_address_list($input, $decode, $fallback); - $out = array(); - $j = 0; - - // Special chars as defined by RFC 822 need to in quoted string (or escaped). - $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]'; - - if (!is_array($a)) - return $out; - - foreach ($a as $val) { - $j++; - $address = trim($val['address']); - $name = trim($val['name']); - - if ($name && $address && $name != $address) - $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address); - else if ($address) - $string = $address; - else if ($name) - $string = $name; - - $out[$j] = array( - 'name' => $name, - 'mailto' => $address, - 'string' => $string - ); - - if ($max && $j==$max) - break; - } - - return $out; - } - - - /** - * Decode a message header value - * - * @param string $input Header value - * @param string $fallback Fallback charset if none specified - * - * @return string Decoded string - */ - public static function decode_header($input, $fallback = null) - { - $str = self::decode_mime_string((string)$input, $fallback); - - return $str; - } - - - /** - * Decode a mime-encoded string to internal charset - * - * @param string $input Header value - * @param string $fallback Fallback charset if none specified - * - * @return string Decoded string - */ - public static function decode_mime_string($input, $fallback = null) - { - $default_charset = !empty($fallback) ? $fallback : self::get_charset(); - - // rfc: all line breaks or other characters not found - // in the Base64 Alphabet must be ignored by decoding software - // delete all blanks between MIME-lines, differently we can - // receive unnecessary blanks and broken utf-8 symbols - $input = preg_replace("/\?=\s+=\?/", '?==?', $input); - - // encoded-word regexp - $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/'; - - // Find all RFC2047's encoded words - if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { - // Initialize variables - $tmp = array(); - $out = ''; - $start = 0; - - foreach ($matches as $idx => $m) { - $pos = $m[0][1]; - $charset = $m[1][0]; - $encoding = $m[2][0]; - $text = $m[3][0]; - $length = strlen($m[0][0]); - - // Append everything that is before the text to be decoded - if ($start != $pos) { - $substr = substr($input, $start, $pos-$start); - $out .= rcube_charset::convert($substr, $default_charset); - $start = $pos; - } - $start += $length; - - // Per RFC2047, each string part "MUST represent an integral number - // of characters . A multi-octet character may not be split across - // adjacent encoded-words." However, some mailers break this, so we - // try to handle characters spanned across parts anyway by iterating - // through and aggregating sequential encoded parts with the same - // character set and encoding, then perform the decoding on the - // aggregation as a whole. - - $tmp[] = $text; - if ($next_match = $matches[$idx+1]) { - if ($next_match[0][1] == $start - && $next_match[1][0] == $charset - && $next_match[2][0] == $encoding - ) { - continue; - } - } - - $count = count($tmp); - $text = ''; - - // Decode and join encoded-word's chunks - if ($encoding == 'B' || $encoding == 'b') { - // base64 must be decoded a segment at a time - for ($i=0; $i<$count; $i++) - $text .= base64_decode($tmp[$i]); - } - else { //if ($encoding == 'Q' || $encoding == 'q') { - // quoted printable can be combined and processed at once - for ($i=0; $i<$count; $i++) - $text .= $tmp[$i]; - - $text = str_replace('_', ' ', $text); - $text = quoted_printable_decode($text); - } - - $out .= rcube_charset::convert($text, $charset); - $tmp = array(); - } - - // add the last part of the input string - if ($start != strlen($input)) { - $out .= rcube_charset::convert(substr($input, $start), $default_charset); - } - - // return the results - return $out; - } - - // no encoding information, use fallback - return rcube_charset::convert($input, $default_charset); - } - - - /** - * Decode a mime part - * - * @param string $input Input string - * @param string $encoding Part encoding - * @return string Decoded string - */ - public static function decode($input, $encoding = '7bit') - { - switch (strtolower($encoding)) { - case 'quoted-printable': - return quoted_printable_decode($input); - case 'base64': - return base64_decode($input); - case 'x-uuencode': - case 'x-uue': - case 'uue': - case 'uuencode': - return convert_uudecode($input); - case '7bit': - default: - return $input; - } - } - - - /** - * Split RFC822 header string into an associative array - * @access private - */ - public static function parse_headers($headers) - { - $a_headers = array(); - $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers); - $lines = explode("\n", $headers); - $c = count($lines); - - for ($i=0; $i<$c; $i++) { - if ($p = strpos($lines[$i], ': ')) { - $field = strtolower(substr($lines[$i], 0, $p)); - $value = trim(substr($lines[$i], $p+1)); - if (!empty($value)) - $a_headers[$field] = $value; - } - } - - return $a_headers; - } - - - /** - * @access private - */ - private static function parse_address_list($str, $decode = true, $fallback = null) - { - // remove any newlines and carriage returns before - $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str); - - // extract list items, remove comments - $str = self::explode_header_string(',;', $str, true); - $result = array(); - - // simplified regexp, supporting quoted local part - $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+'; - - foreach ($str as $key => $val) { - $name = ''; - $address = ''; - $val = trim($val); - - if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) { - $address = $m[2]; - $name = trim($m[1]); - } - else if (preg_match('/^('.$email_rx.')$/', $val, $m)) { - $address = $m[1]; - $name = ''; - } - else { - $name = $val; - } - - // dequote and/or decode name - if ($name) { - if ($name[0] == '"' && $name[strlen($name)-1] == '"') { - $name = substr($name, 1, -1); - $name = stripslashes($name); - } - if ($decode) { - $name = self::decode_header($name, $fallback); - } - } - - if (!$address && $name) { - $address = $name; - } - - if ($address) { - $result[$key] = array('name' => $name, 'address' => $address); - } - } - - return $result; - } - - - /** - * Explodes header (e.g. address-list) string into array of strings - * using specified separator characters with proper handling - * of quoted-strings and comments (RFC2822) - * - * @param string $separator String containing separator characters - * @param string $str Header string - * @param bool $remove_comments Enable to remove comments - * - * @return array Header items - */ - public static function explode_header_string($separator, $str, $remove_comments = false) - { - $length = strlen($str); - $result = array(); - $quoted = false; - $comment = 0; - $out = ''; - - for ($i=0; $i<$length; $i++) { - // we're inside a quoted string - if ($quoted) { - if ($str[$i] == '"') { - $quoted = false; - } - else if ($str[$i] == "\\") { - if ($comment <= 0) { - $out .= "\\"; - } - $i++; - } - } - // we are inside a comment string - else if ($comment > 0) { - if ($str[$i] == ')') { - $comment--; - } - else if ($str[$i] == '(') { - $comment++; - } - else if ($str[$i] == "\\") { - $i++; - } - continue; - } - // separator, add to result array - else if (strpos($separator, $str[$i]) !== false) { - if ($out) { - $result[] = $out; - } - $out = ''; - continue; - } - // start of quoted string - else if ($str[$i] == '"') { - $quoted = true; - } - // start of comment - else if ($remove_comments && $str[$i] == '(') { - $comment++; - } - - if ($comment <= 0) { - $out .= $str[$i]; - } - } - - if ($out && $comment <= 0) { - $result[] = $out; - } - - return $result; - } - - - /** - * Interpret a format=flowed message body according to RFC 2646 - * - * @param string $text Raw body formatted as flowed text - * - * @return string Interpreted text with unwrapped lines and stuffed space removed - */ - public static function unfold_flowed($text) - { - $text = preg_split('/\r?\n/', $text); - $last = -1; - $q_level = 0; - - foreach ($text as $idx => $line) { - if ($line[0] == '>' && preg_match('/^(>+\s*)/', $line, $regs)) { - $q = strlen(str_replace(' ', '', $regs[0])); - $line = substr($line, strlen($regs[0])); - - if ($q == $q_level && $line - && isset($text[$last]) - && $text[$last][strlen($text[$last])-1] == ' ' - ) { - $text[$last] .= $line; - unset($text[$idx]); - } - else { - $last = $idx; - } - } - else { - $q = 0; - if ($line == '-- ') { - $last = $idx; - } - else { - // remove space-stuffing - $line = preg_replace('/^\s/', '', $line); - - if (isset($text[$last]) && $line - && $text[$last] != '-- ' - && $text[$last][strlen($text[$last])-1] == ' ' - ) { - $text[$last] .= $line; - unset($text[$idx]); - } - else { - $text[$idx] = $line; - $last = $idx; - } - } - } - $q_level = $q; - } - - return implode("\r\n", $text); - } - - - /** - * Wrap the given text to comply with RFC 2646 - * - * @param string $text Text to wrap - * @param int $length Length - * - * @return string Wrapped text - */ - public static function format_flowed($text, $length = 72) - { - $text = preg_split('/\r?\n/', $text); - - foreach ($text as $idx => $line) { - if ($line != '-- ') { - if ($line[0] == '>' && preg_match('/^(>+)/', $line, $regs)) { - $prefix = $regs[0]; - $level = strlen($prefix); - $line = rtrim(substr($line, $level)); - $line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix "); - } - else if ($line) { - $line = self::wordwrap(rtrim($line), $length - 2, " \r\n"); - // space-stuffing - $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line); - } - - $text[$idx] = $line; - } - } - - return implode("\r\n", $text); - } - - - /** - * Improved wordwrap function. - * - * @param string $string Text to wrap - * @param int $width Line width - * @param string $break Line separator - * @param bool $cut Enable to cut word - * - * @return string Text - */ - public static function wordwrap($string, $width=75, $break="\n", $cut=false) - { - $para = explode($break, $string); - $string = ''; - - while (count($para)) { - $line = array_shift($para); - if ($line[0] == '>') { - $string .= $line.$break; - continue; - } - - $list = explode(' ', $line); - $len = 0; - while (count($list)) { - $line = array_shift($list); - $l = mb_strlen($line); - $newlen = $len + $l + ($len ? 1 : 0); - - if ($newlen <= $width) { - $string .= ($len ? ' ' : '').$line; - $len += (1 + $l); - } - else { - if ($l > $width) { - if ($cut) { - $start = 0; - while ($l) { - $str = mb_substr($line, $start, $width); - $strlen = mb_strlen($str); - $string .= ($len ? $break : '').$str; - $start += $strlen; - $l -= $strlen; - $len = $strlen; - } - } - else { - $string .= ($len ? $break : '').$line; - if (count($list)) { - $string .= $break; - } - $len = 0; - } - } - else { - $string .= $break.$line; - $len = $l; - } - } - } - - if (count($para)) { - $string .= $break; - } - } - - return $string; - } - - - /** - * A method to guess the mime_type of an attachment. - * - * @param string $path Path to the file. - * @param string $name File name (with suffix) - * @param string $failover Mime type supplied for failover. - * @param string $is_stream Set to True if $path contains file body - * - * @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) - { - $mime_type = null; - $mime_magic = rcube::get_instance()->config->get('mime_magic'); - $mime_ext = @include RCMAIL_CONFIG_DIR . '/mimetypes.php'; - - // use file name suffix with hard-coded mime-type map - if (is_array($mime_ext) && $name) { - if ($suffix = substr($name, strrpos($name, '.')+1)) { - $mime_type = $mime_ext[strtolower($suffix)]; - } - } - - // try fileinfo extension if available - if (!$mime_type && function_exists('finfo_open')) { - if ($finfo = finfo_open(FILEINFO_MIME, $mime_magic)) { - if ($is_stream) - $mime_type = finfo_buffer($finfo, $path); - else - $mime_type = finfo_file($finfo, $path); - finfo_close($finfo); - } - } - - // try PHP's mime_content_type - if (!$mime_type && !$is_stream && function_exists('mime_content_type')) { - $mime_type = @mime_content_type($path); - } - - // fall back to user-submitted string - if (!$mime_type) { - $mime_type = $failover; - } - else { - // Sometimes (PHP-5.3?) content-type contains charset definition, - // Remove it (#1487122) also "charset=binary" is useless - $mime_type = array_shift(preg_split('/[; ]/', $mime_type)); - } - - return $mime_type; - } - - - /** - * 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; - } - -} diff --git a/program/include/rcube_output.php b/program/include/rcube_output.php deleted file mode 100644 index 5c582e67c..000000000 --- a/program/include/rcube_output.php +++ /dev/null @@ -1,283 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_output.php | - | | - | This file is part of the Roundcube PHP suite | - | Copyright (C) 2005-2012 The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | CONTENTS: | - | Abstract class for output generation | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - -/** - * Class for output generation - * - * @package HTML - */ -abstract class rcube_output -{ - public $browser; - public $type = 'html'; - public $ajax_call = false; - public $framed = false; - - protected $app; - protected $config; - protected $charset = RCMAIL_CHARSET; - protected $env = array(); - protected $pagetitle = ''; - protected $object_handlers = array(); - - - /** - * Object constructor - */ - public function __construct($task = null, $framed = false) - { - $this->app = rcube::get_instance(); - $this->config = $this->app->config; - $this->browser = new rcube_browser(); - } - - - /** - * Magic getter - */ - public function __get($var) - { - // allow read-only access to $env - if ($var == 'env') - return $this->env; - - return null; - } - - /** - * Setter for page title - * - * @param string $title Page title - */ - public function set_pagetitle($title) - { - $this->pagetitle = $title; - } - - - /** - * 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; - } - - - /** - * Getter for the current skin path property - */ - public function get_skin_path() - { - return $this->config->get('skin_path'); - } - - - /** - * Set environment variable - * - * @param string $name Property name - * @param mixed $value Property value - */ - public function set_env($name, $value) - { - $this->env[$name] = $value; - } - - - /** - * Environment variable getter. - * - * @param string $name Property name - * - * @return mixed Property value - */ - public function get_env($name) - { - return $this->env[$name]; - } - - - /** - * Delete all stored env variables and commands - */ - public function reset() - { - $this->env = array(); - $this->object_handlers = array(); - $this->pagetitle = ''; - } - - - /** - * Call a client method - * - * @param string Method to call - * @param ... Additional arguments - */ - abstract function command(); - - - /** - * Add a localized label to the client environment - */ - abstract function add_label(); - - - /** - * Invoke display_message command - * - * @param string $message Message to display - * @param string $type Message type [notice|confirm|error] - * @param array $vars Key-value pairs to be replaced in localized text - * @param boolean $override Override last set message - * @param int $timeout Message displaying time in seconds - */ - abstract function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0); - - - /** - * Redirect to a certain url. - * - * @param mixed $p Either a string with the action or url parameters as key-value pairs - * @param int $delay Delay in seconds - */ - abstract function redirect($p = array(), $delay = 1); - - - /** - * Send output to the client. - */ - abstract function send(); - - - /** - * Register a template object handler - * - * @param string Object name - * @param string Function name to call - * @return void - */ - public function add_handler($obj, $func) - { - $this->object_handlers[$obj] = $func; - } - - - /** - * Register a list of template object handlers - * - * @param array Hash array with object=>handler pairs - * @return void - */ - public function add_handlers($arr) - { - $this->object_handlers = array_merge($this->object_handlers, $arr); - } - - - /** - * Send HTTP headers to prevent caching a page - */ - public function nocacheing_headers() - { - if (headers_sent()) { - return; - } - - header("Expires: ".gmdate("D, d M Y H:i:s")." GMT"); - header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT"); - - // Request browser to disable DNS prefetching (CVE-2010-0464) - header("X-DNS-Prefetch-Control: off"); - - // We need to set the following headers to make downloads work using IE in HTTPS mode. - if ($this->browser->ie && rcube_utils::https_check()) { - header('Pragma: private'); - header("Cache-Control: private, must-revalidate"); - } - else { - header("Cache-Control: private, no-cache, 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: "); - } - - - /** - * Show error page and terminate script execution - * - * @param int $code Error code - * @param string $message Error message - */ - public function raise_error($code, $message) - { - // STUB: to be overloaded by specific output classes - fputs(STDERR, "Error $code: $message\n"); - exit(-1); - } - - - /** - * Convert a variable into a javascript object notation - * - * @param mixed Input value - * - * @return string Serialized JSON string - */ - public static function json_serialize($input) - { - $input = rcube_charset::clean($input); - - // sometimes even using rcube_charset::clean() the input contains invalid UTF-8 sequences - // that's why we have @ here - return @json_encode($input); - } - -} diff --git a/program/include/rcube_plugin.php b/program/include/rcube_plugin.php deleted file mode 100644 index 45088850a..000000000 --- a/program/include/rcube_plugin.php +++ /dev/null @@ -1,359 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_plugin.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2009, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Abstract plugins interface/class | - | All plugins need to extend this class | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - -/** - * Plugin interface class - * - * @package PluginAPI - */ -abstract class rcube_plugin -{ - /** - * Class name of the plugin instance - * - * @var string - */ - public $ID; - - /** - * Instance of Plugin API - * - * @var rcube_plugin_api - */ - public $api; - - /** - * Regular expression defining task(s) to bind with - * - * @var string - */ - public $task; - - /** - * Disables plugin in AJAX requests - * - * @var boolean - */ - public $noajax = false; - - /** - * Disables plugin in framed mode - * - * @var boolean - */ - public $noframe = false; - - protected $home; - protected $urlbase; - private $mytask; - - - /** - * Default constructor. - * - * @param rcube_plugin_api $api Plugin API - */ - public function __construct($api) - { - $this->ID = get_class($this); - $this->api = $api; - $this->home = $api->dir . $this->ID; - $this->urlbase = $api->url . $this->ID . '/'; - } - - /** - * Initialization method, needs to be implemented by the plugin itself - */ - abstract function init(); - - - /** - * Attempt to load the given plugin which is required for the current plugin - * - * @param string Plugin name - * @return boolean True on success, false on failure - */ - public function require_plugin($plugin_name) - { - return $this->api->load_plugin($plugin_name); - } - - - /** - * Load local config file from plugins directory. - * The loaded values are patched over the global configuration. - * - * @param string $fname Config file name relative to the plugin's folder - * @return boolean True on success, false on failure - */ - public function load_config($fname = 'config.inc.php') - { - $fpath = $this->home.'/'.$fname; - $rcube = rcube::get_instance(); - if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) { - rcube::raise_error(array( - 'code' => 527, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to load config from $fpath"), true, false); - return false; - } - - return true; - } - - /** - * Register a callback function for a specific (server-side) hook - * - * @param string $hook Hook name - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function add_hook($hook, $callback) - { - $this->api->register_hook($hook, $callback); - } - - /** - * Unregister a callback function for a specific (server-side) hook. - * - * @param string $hook Hook name - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function remove_hook($hook, $callback) - { - $this->api->unregister_hook($hook, $callback); - } - - /** - * Load localized texts from the plugins dir - * - * @param string $dir Directory to search in - * @param mixed $add2client Make texts also available on the client (array with list or true for all) - */ - public function add_texts($dir, $add2client = false) - { - $domain = $this->ID; - $lang = $_SESSION['language']; - $langs = array_unique(array('en_US', $lang)); - $locdir = slashify(realpath(slashify($this->home) . $dir)); - $texts = array(); - - // Language aliases used to find localization in similar lang, see below - $aliases = array( - 'de_CH' => 'de_DE', - 'es_AR' => 'es_ES', - 'fa_AF' => 'fa_IR', - 'nl_BE' => 'nl_NL', - 'pt_BR' => 'pt_PT', - 'zh_CN' => 'zh_TW', - ); - - // use buffering to handle empty lines/spaces after closing PHP tag - ob_start(); - - foreach ($langs as $lng) { - $fpath = $locdir . $lng . '.inc'; - if (is_file($fpath) && is_readable($fpath)) { - include $fpath; - $texts = (array)$labels + (array)$messages + (array)$texts; - } - else if ($lng != 'en_US') { - // Find localization in similar language (#1488401) - $alias = null; - if (!empty($aliases[$lng])) { - $alias = $aliases[$lng]; - } - else if ($key = array_search($lng, $aliases)) { - $alias = $key; - } - - if (!empty($alias)) { - $fpath = $locdir . $alias . '.inc'; - if (is_file($fpath) && is_readable($fpath)) { - include $fpath; - $texts = (array)$labels + (array)$messages + (array)$texts; - } - } - } - } - - ob_end_clean(); - - // prepend domain to text keys and add to the application texts repository - if (!empty($texts)) { - $add = array(); - foreach ($texts as $key => $value) - $add[$domain.'.'.$key] = $value; - - $rcmail = rcube::get_instance(); - $rcmail->load_language($lang, $add); - - // add labels to client - if ($add2client) { - $js_labels = is_array($add2client) ? array_map(array($this, 'label_map_callback'), $add2client) : array_keys($add); - $rcmail->output->add_label($js_labels); - } - } - } - - /** - * Wrapper for rcmail::gettext() adding the plugin ID as domain - * - * @param string $p Message identifier - * @return string Localized text - * @see rcmail::gettext() - */ - public function gettext($p) - { - return rcube::get_instance()->gettext($p, $this->ID); - } - - /** - * Register this plugin to be responsible for a specific task - * - * @param string $task Task name (only characters [a-z0-9_.-] are allowed) - */ - public function register_task($task) - { - if ($this->api->register_task($task, $this->ID)) - $this->mytask = $task; - } - - /** - * Register a handler for a specific client-request action - * - * The callback will be executed upon a request like /?_task=mail&_action=plugin.myaction - * - * @param string $action Action name (should be unique) - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function register_action($action, $callback) - { - $this->api->register_action($action, $this->ID, $callback, $this->mytask); - } - - /** - * Register a handler function for a template object - * - * When parsing a template for display, tags like <roundcube:object name="plugin.myobject" /> - * will be replaced by the return value if the registered callback function. - * - * @param string $name Object name (should be unique and start with 'plugin.') - * @param mixed $callback Callback function as string or array with object reference and method name - */ - public function register_handler($name, $callback) - { - $this->api->register_handler($name, $this->ID, $callback); - } - - /** - * Make this javascipt file available on the client - * - * @param string $fn File path; absolute or relative to the plugin directory - */ - public function include_script($fn) - { - $this->api->include_script($this->resource_url($fn)); - } - - /** - * Make this stylesheet available on the client - * - * @param string $fn File path; absolute or relative to the plugin directory - */ - public function include_stylesheet($fn) - { - $this->api->include_stylesheet($this->resource_url($fn)); - } - - /** - * Append a button to a certain container - * - * @param array $p Hash array with named parameters (as used in skin templates) - * @param string $container Container name where the buttons should be added to - * @see rcube_remplate::button() - */ - public function add_button($p, $container) - { - if ($this->api->output->type == 'html') { - // fix relative paths - foreach (array('imagepas', 'imageact', 'imagesel') as $key) - if ($p[$key]) - $p[$key] = $this->api->url . $this->resource_url($p[$key]); - - $this->api->add_content($this->api->output->button($p), $container); - } - } - - /** - * Generate an absolute URL to the given resource within the current - * plugin directory - * - * @param string $fn The file name - * @return string Absolute URL to the given resource - */ - public function url($fn) - { - return $this->api->url . $this->resource_url($fn); - } - - /** - * Make the given file name link into the plugin directory - * - * @param string $fn Filename - */ - private function resource_url($fn) - { - if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) - return $this->ID . '/' . $fn; - else - return $fn; - } - - /** - * Provide path to the currently selected skin folder within the plugin directory - * with a fallback to the default skin folder. - * - * @return string Skin path relative to plugins directory - */ - public function local_skin_path() - { - $rcmail = rcube::get_instance(); - foreach (array($rcmail->config->get('skin'), 'larry') as $skin) { - $skin_path = 'skins/' . $skin; - if (is_dir(realpath(slashify($this->home) . $skin_path))) - break; - } - - return $skin_path; - } - - /** - * Callback function for array_map - * - * @param string $key Array key. - * @return string - */ - private function label_map_callback($key) - { - return $this->ID.'.'.$key; - } - -} diff --git a/program/include/rcube_plugin_api.php b/program/include/rcube_plugin_api.php deleted file mode 100644 index 107c81026..000000000 --- a/program/include/rcube_plugin_api.php +++ /dev/null @@ -1,494 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_plugin_api.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2011, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Plugins repository | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - -// location where plugins are loade from -if (!defined('RCMAIL_PLUGINS_DIR')) - define('RCMAIL_PLUGINS_DIR', INSTALL_PATH . 'plugins/'); - - -/** - * The plugin loader and global API - * - * @package PluginAPI - */ -class rcube_plugin_api -{ - static protected $instance; - - public $dir; - public $url = 'plugins/'; - public $task = ''; - public $output; - - public $handlers = array(); - protected $plugins = array(); - protected $tasks = array(); - protected $actions = array(); - protected $actionmap = array(); - protected $objectsmap = array(); - protected $template_contents = array(); - protected $active_hook = false; - - // Deprecated names of hooks, will be removed after 0.5-stable release - protected $deprecated_hooks = array( - 'create_user' => 'user_create', - 'kill_session' => 'session_destroy', - 'upload_attachment' => 'attachment_upload', - 'save_attachment' => 'attachment_save', - 'get_attachment' => 'attachment_get', - 'cleanup_attachments' => 'attachments_cleanup', - 'display_attachment' => 'attachment_display', - 'remove_attachment' => 'attachment_delete', - 'outgoing_message_headers' => 'message_outgoing_headers', - 'outgoing_message_body' => 'message_outgoing_body', - 'address_sources' => 'addressbooks_list', - 'get_address_book' => 'addressbook_get', - 'create_contact' => 'contact_create', - 'save_contact' => 'contact_update', - 'contact_save' => 'contact_update', - 'delete_contact' => 'contact_delete', - 'manage_folders' => 'folders_list', - 'list_mailboxes' => 'mailboxes_list', - 'save_preferences' => 'preferences_save', - 'user_preferences' => 'preferences_list', - 'list_prefs_sections' => 'preferences_sections_list', - 'list_identities' => 'identities_list', - 'create_identity' => 'identity_create', - 'delete_identity' => 'identity_delete', - 'save_identity' => 'identity_update', - 'identity_save' => 'identity_update', - // to be removed after 0.8 - 'imap_init' => 'storage_init', - 'mailboxes_list' => 'storage_folders', - ); - - /** - * 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(RCMAIL_PLUGINS_DIR); - } - - - /** - * Initialize plugin engine - * - * This has to be done after rcmail::load_gui() or rcmail::json_init() - * was called because plugins need to have access to rcmail->output - * - * @param object rcube Instance of the rcube base class - * @param string Current application task (used for conditional plugin loading) - */ - public function init($app, $task = '') - { - $this->task = $task; - $this->output = $app->output; - - // register an internal hook - $this->register_hook('template_container', array($this, 'template_container_hook')); - - // maybe also register a shudown function which triggers shutdown functions of all plugin objects - } - - - /** - * Load and init all enabled plugins - * - * This has to be done after rcmail::load_gui() or rcmail::json_init() - * was called because plugins need to have access to rcmail->output - * - * @param array List of configured plugins to load - * @param array List of plugins required by the application - */ - public function load_plugins($plugins_enabled, $required_plugins = array()) - { - foreach ($plugins_enabled as $plugin_name) { - $this->load_plugin($plugin_name); - } - - // check existance of all required core plugins - foreach ($required_plugins as $plugin_name) { - $loaded = false; - foreach ($this->plugins as $plugin) { - if ($plugin instanceof $plugin_name) { - $loaded = true; - break; - } - } - - // load required core plugin if no derivate was found - if (!$loaded) - $loaded = $this->load_plugin($plugin_name); - - // trigger fatal error if still not loaded - if (!$loaded) { - rcube::raise_error(array('code' => 520, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Requried plugin $plugin_name was not loaded"), true, true); - } - } - } - - /** - * Load the specified plugin - * - * @param string Plugin name - * @return boolean True on success, false if not loaded or failure - */ - public function load_plugin($plugin_name) - { - static $plugins_dir; - - if (!$plugins_dir) { - $dir = dir($this->dir); - $plugins_dir = unslashify($dir->path); - } - - // plugin already loaded - if ($this->plugins[$plugin_name] || class_exists($plugin_name, false)) - return true; - - $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php'; - - if (file_exists($fn)) { - include($fn); - - // instantiate class if exists - if (class_exists($plugin_name, false)) { - $plugin = new $plugin_name($this); - // check inheritance... - if (is_subclass_of($plugin, 'rcube_plugin')) { - // ... task, request type and framed mode - if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task)) - && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html')) - && (!$plugin->noframe || empty($_REQUEST['_framed'])) - ) { - $plugin->init(); - $this->plugins[$plugin_name] = $plugin; - } - return true; - } - } - else { - rcube::raise_error(array('code' => 520, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "No plugin class $plugin_name found in $fn"), true, false); - } - } - else { - rcube::raise_error(array('code' => 520, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Failed to load plugin file $fn"), true, false); - } - - return false; - } - - - /** - * Allows a plugin object to register a callback for a certain hook - * - * @param string $hook Hook name - * @param mixed $callback String with global function name or array($obj, 'methodname') - */ - public function register_hook($hook, $callback) - { - if (is_callable($callback)) { - if (isset($this->deprecated_hooks[$hook])) { - rcube::raise_error(array('code' => 522, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Deprecated hook name. ".$hook.' -> '.$this->deprecated_hooks[$hook]), true, false); - $hook = $this->deprecated_hooks[$hook]; - } - $this->handlers[$hook][] = $callback; - } - else - rcube::raise_error(array('code' => 521, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Invalid callback function for $hook"), true, false); - } - - /** - * Allow a plugin object to unregister a callback. - * - * @param string $hook Hook name - * @param mixed $callback String with global function name or array($obj, 'methodname') - */ - public function unregister_hook($hook, $callback) - { - $callback_id = array_search($callback, $this->handlers[$hook]); - if ($callback_id !== false) { - unset($this->handlers[$hook][$callback_id]); - } - } - - - /** - * Triggers a plugin hook. - * This is called from the application and executes all registered handlers - * - * @param string $hook Hook name - * @param array $args Named arguments (key->value pairs) - * @return array The (probably) altered hook arguments - */ - public function exec_hook($hook, $args = array()) - { - if (!is_array($args)) - $args = array('arg' => $args); - - $args += array('abort' => false); - $this->active_hook = $hook; - - foreach ((array)$this->handlers[$hook] as $callback) { - $ret = call_user_func($callback, $args); - if ($ret && is_array($ret)) - $args = $ret + $args; - - if ($args['abort']) - break; - } - - $this->active_hook = false; - return $args; - } - - - /** - * Let a plugin register a handler for a specific request - * - * @param string $action Action name (_task=mail&_action=plugin.foo) - * @param string $owner Plugin name that registers this action - * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') - * @param string $task Task name registered by this plugin - */ - public function register_action($action, $owner, $callback, $task = null) - { - // check action name - if ($task) - $action = $task.'.'.$action; - else if (strpos($action, 'plugin.') !== 0) - $action = 'plugin.'.$action; - - // can register action only if it's not taken or registered by myself - if (!isset($this->actionmap[$action]) || $this->actionmap[$action] == $owner) { - $this->actions[$action] = $callback; - $this->actionmap[$action] = $owner; - } - else { - rcube::raise_error(array('code' => 523, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Cannot register action $action; already taken by another plugin"), true, false); - } - } - - - /** - * This method handles requests like _task=mail&_action=plugin.foo - * It executes the callback function that was registered with the given action. - * - * @param string $action Action name - */ - public function exec_action($action) - { - if (isset($this->actions[$action])) { - call_user_func($this->actions[$action]); - } - else { - rcube::raise_error(array('code' => 524, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "No handler found for action $action"), true, true); - } - } - - - /** - * Register a handler function for template objects - * - * @param string $name Object name - * @param string $owner Plugin name that registers this action - * @param mixed $callback Callback: string with global function name or array($obj, 'methodname') - */ - public function register_handler($name, $owner, $callback) - { - // check name - if (strpos($name, 'plugin.') !== 0) - $name = 'plugin.'.$name; - - // can register handler only if it's not taken or registered by myself - if (is_object($this->output) && (!isset($this->objectsmap[$name]) || $this->objectsmap[$name] == $owner)) { - $this->output->add_handler($name, $callback); - $this->objectsmap[$name] = $owner; - } - else { - rcube::raise_error(array('code' => 525, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Cannot register template handler $name; already taken by another plugin or no output object available"), true, false); - } - } - - - /** - * Register this plugin to be responsible for a specific task - * - * @param string $task Task name (only characters [a-z0-9_.-] are allowed) - * @param string $owner Plugin name that registers this action - */ - public function register_task($task, $owner) - { - if ($task != asciiwords($task)) { - rcube::raise_error(array('code' => 526, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Invalid task name: $task. Only characters [a-z0-9_.-] are allowed"), true, false); - } - else if (in_array($task, rcmail::$main_tasks)) { - rcube::raise_error(array('code' => 526, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Cannot register taks $task; already taken by another plugin or the application itself"), true, false); - } - else { - $this->tasks[$task] = $owner; - rcmail::$main_tasks[] = $task; - return true; - } - - return false; - } - - - /** - * Checks whether the given task is registered by a plugin - * - * @param string $task Task name - * @return boolean True if registered, otherwise false - */ - public function is_plugin_task($task) - { - return $this->tasks[$task] ? true : false; - } - - - /** - * Check if a plugin hook is currently processing. - * Mainly used to prevent loops and recursion. - * - * @param string $hook Hook to check (optional) - * @return boolean True if any/the given hook is currently processed, otherwise false - */ - public function is_processing($hook = null) - { - return $this->active_hook && (!$hook || $this->active_hook == $hook); - } - - /** - * Include a plugin script file in the current HTML page - * - * @param string $fn Path to script - */ - public function include_script($fn) - { - if (is_object($this->output) && $this->output->type == 'html') { - $src = $this->resource_url($fn); - $this->output->add_header(html::tag('script', array('type' => "text/javascript", 'src' => $src))); - } - } - - - /** - * Include a plugin stylesheet in the current HTML page - * - * @param string $fn Path to stylesheet - */ - public function include_stylesheet($fn) - { - if (is_object($this->output) && $this->output->type == 'html') { - $src = $this->resource_url($fn); - $this->output->include_css($src); - } - } - - - /** - * Save the given HTML content to be added to a template container - * - * @param string $html HTML content - * @param string $container Template container identifier - */ - public function add_content($html, $container) - { - $this->template_contents[$container] .= $html . "\n"; - } - - - /** - * Returns list of loaded plugins names - * - * @return array List of plugin names - */ - public function loaded_plugins() - { - return array_keys($this->plugins); - } - - - /** - * Callback for template_container hooks - * - * @param array $attrib - * @return array - */ - protected function template_container_hook($attrib) - { - $container = $attrib['name']; - return array('content' => $attrib['content'] . $this->template_contents[$container]); - } - - - /** - * Make the given file name link into the plugins directory - * - * @param string $fn Filename - * @return string - */ - protected function resource_url($fn) - { - if ($fn[0] != '/' && !preg_match('|^https?://|i', $fn)) - return $this->url . $fn; - else - return $fn; - } - -} diff --git a/program/include/rcube_result_index.php b/program/include/rcube_result_index.php deleted file mode 100644 index 334ec8530..000000000 --- a/program/include/rcube_result_index.php +++ /dev/null @@ -1,450 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_result_index.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2011, The Roundcube Dev Team | - | Copyright (C) 2011, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | SORT/SEARCH/ESEARCH response handler | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Class for accessing IMAP's SORT/SEARCH/ESEARCH result - */ -class rcube_result_index -{ - protected $raw_data; - protected $mailbox; - protected $meta = array(); - protected $params = array(); - protected $order = 'ASC'; - - const SEPARATOR_ELEMENT = ' '; - - - /** - * Object constructor. - */ - public function __construct($mailbox = null, $data = null) - { - $this->mailbox = $mailbox; - $this->init($data); - } - - - /** - * Initializes object with SORT command response - * - * @param string $data IMAP response string - */ - public function init($data = null) - { - $this->meta = array(); - - $data = explode('*', (string)$data); - - // ...skip unilateral untagged server responses - for ($i=0, $len=count($data); $i<$len; $i++) { - $data_item = &$data[$i]; - if (preg_match('/^ SORT/i', $data_item)) { - // valid response, initialize raw_data for is_error() - $this->raw_data = ''; - $data_item = substr($data_item, 5); - break; - } - else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) { - // valid response, initialize raw_data for is_error() - $this->raw_data = ''; - $data_item = substr($data_item, strlen($m[0])); - - if (strtoupper($m[1]) == 'ESEARCH') { - $data_item = trim($data_item); - // remove MODSEQ response - if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) { - $data_item = substr($data_item, 0, -strlen($m[0])); - $this->params['MODSEQ'] = $m[1]; - } - // remove TAG response part - if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) { - $data_item = substr($data_item, strlen($m[0])); - } - // remove UID - $data_item = preg_replace('/^UID\s*/i', '', $data_item); - - // ESEARCH parameters - while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) { - $param = strtoupper($m[1]); - $value = $m[2]; - - $this->params[$param] = $value; - $data_item = substr($data_item, strlen($m[0])); - - if (in_array($param, array('COUNT', 'MIN', 'MAX'))) { - $this->meta[strtolower($param)] = (int) $value; - } - } - -// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ? -// @TODO: work with compressed result?! - if (isset($this->params['ALL'])) { - $data_item = implode(self::SEPARATOR_ELEMENT, - rcube_imap_generic::uncompressMessageSet($this->params['ALL'])); - } - } - - break; - } - - unset($data[$i]); - } - - $data = array_filter($data); - - if (empty($data)) { - return; - } - - $data = array_shift($data); - $data = trim($data); - $data = preg_replace('/[\r\n]/', '', $data); - $data = preg_replace('/\s+/', ' ', $data); - - $this->raw_data = $data; - } - - - /** - * Checks the result from IMAP command - * - * @return bool True if the result is an error, False otherwise - */ - public function is_error() - { - return $this->raw_data === null ? true : false; - } - - - /** - * Checks if the result is empty - * - * @return bool True if the result is empty, False otherwise - */ - public function is_empty() - { - return empty($this->raw_data) ? true : false; - } - - - /** - * Returns number of elements in the result - * - * @return int Number of elements - */ - public function count() - { - if ($this->meta['count'] !== null) - return $this->meta['count']; - - if (empty($this->raw_data)) { - $this->meta['count'] = 0; - $this->meta['length'] = 0; - } - else { - $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); - } - - return $this->meta['count']; - } - - - /** - * Returns number of elements in the result. - * Alias for count() for compatibility with rcube_result_thread - * - * @return int Number of elements - */ - public function count_messages() - { - return $this->count(); - } - - - /** - * Returns maximal message identifier in the result - * - * @return int Maximal message identifier - */ - public function max() - { - if (!isset($this->meta['max'])) { - $this->meta['max'] = (int) @max($this->get()); - } - - return $this->meta['max']; - } - - - /** - * Returns minimal message identifier in the result - * - * @return int Minimal message identifier - */ - public function min() - { - if (!isset($this->meta['min'])) { - $this->meta['min'] = (int) @min($this->get()); - } - - return $this->meta['min']; - } - - - /** - * Slices data set. - * - * @param $offset Offset (as for PHP's array_slice()) - * @param $length Number of elements (as for PHP's array_slice()) - * - */ - public function slice($offset, $length) - { - $data = $this->get(); - $data = array_slice($data, $offset, $length); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - - /** - * Filters data set. Removes elements listed in $ids list. - * - * @param array $ids List of IDs to remove. - */ - public function filter($ids = array()) - { - $data = $this->get(); - $data = array_diff($data, $ids); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - - /** - * Filters data set. Removes elements not listed in $ids list. - * - * @param array $ids List of IDs to keep. - */ - public function intersect($ids = array()) - { - $data = $this->get(); - $data = array_intersect($data, $ids); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - - /** - * Reverts order of elements in the result - */ - public function revert() - { - $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; - - if (empty($this->raw_data)) { - return; - } - - // @TODO: maybe do this in chunks - $data = $this->get(); - $data = array_reverse($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - - $this->meta['pos'] = array(); - } - - - /** - * Check if the given message ID exists in the object - * - * @param int $msgid Message ID - * @param bool $get_index When enabled element's index will be returned. - * Elements are indexed starting with 0 - * - * @return mixed False if message ID doesn't exist, True if exists or - * index of the element if $get_index=true - */ - public function exists($msgid, $get_index = false) - { - if (empty($this->raw_data)) { - return false; - } - - $msgid = (int) $msgid; - $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/'))); - $end = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/'))); - - if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, - $get_index ? PREG_OFFSET_CAPTURE : null) - ) { - if ($get_index) { - $idx = 0; - if ($m[0][1]) { - $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]); - } - // cache position of this element, so we can use it in get_element() - $this->meta['pos'][$idx] = (int)$m[0][1]; - - return $idx; - } - return true; - } - - return false; - } - - - /** - * Return all messages in the result. - * - * @return array List of message IDs - */ - public function get() - { - if (empty($this->raw_data)) { - return array(); - } - return explode(self::SEPARATOR_ELEMENT, $this->raw_data); - } - - - /** - * Return all messages in the result. - * - * @return array List of message IDs - */ - public function get_compressed() - { - if (empty($this->raw_data)) { - return ''; - } - - return rcube_imap_generic::compressMessageSet($this->get()); - } - - - /** - * Return result element at specified index - * - * @param int|string $index Element's index or "FIRST" or "LAST" - * - * @return int Element value - */ - public function get_element($index) - { - $count = $this->count(); - - if (!$count) { - return null; - } - - // first element - if ($index === 0 || $index === '0' || $index === 'FIRST') { - $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT); - if ($pos === false) - $result = (int) $this->raw_data; - else - $result = (int) substr($this->raw_data, 0, $pos); - - return $result; - } - - // last element - if ($index === 'LAST' || $index == $count-1) { - $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT); - if ($pos === false) - $result = (int) $this->raw_data; - else - $result = (int) substr($this->raw_data, $pos); - - return $result; - } - - // do we know the position of the element or the neighbour of it? - if (!empty($this->meta['pos'])) { - if (isset($this->meta['pos'][$index])) - $pos = $this->meta['pos'][$index]; - else if (isset($this->meta['pos'][$index-1])) - $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, - $this->meta['pos'][$index-1] + 1); - else if (isset($this->meta['pos'][$index+1])) - $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT, - $this->meta['pos'][$index+1] - $this->length() - 1); - - if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) { - return (int) $m[1]; - } - } - - // Finally use less effective method - $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); - - return $data[$index]; - } - - - /** - * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ - * or internal data e.g. MAILBOX, ORDER - * - * @param string $param Parameter name - * - * @return array|string Response parameters or parameter value - */ - public function get_parameters($param=null) - { - $params = $this->params; - $params['MAILBOX'] = $this->mailbox; - $params['ORDER'] = $this->order; - - if ($param !== null) { - return $params[$param]; - } - - return $params; - } - - - /** - * Returns length of internal data representation - * - * @return int Data length - */ - protected function length() - { - if (!isset($this->meta['length'])) { - $this->meta['length'] = strlen($this->raw_data); - } - - return $this->meta['length']; - } -} diff --git a/program/include/rcube_result_thread.php b/program/include/rcube_result_thread.php deleted file mode 100644 index b2325a499..000000000 --- a/program/include/rcube_result_thread.php +++ /dev/null @@ -1,673 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_result_thread.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2011, The Roundcube Dev Team | - | Copyright (C) 2011, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | THREAD response handler | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Class for accessing IMAP's THREAD result - */ -class rcube_result_thread -{ - protected $raw_data; - protected $mailbox; - protected $meta = array(); - protected $order = 'ASC'; - - const SEPARATOR_ELEMENT = ' '; - const SEPARATOR_ITEM = '~'; - const SEPARATOR_LEVEL = ':'; - - - /** - * Object constructor. - */ - public function __construct($mailbox = null, $data = null) - { - $this->mailbox = $mailbox; - $this->init($data); - } - - - /** - * Initializes object with IMAP command response - * - * @param string $data IMAP response string - */ - public function init($data = null) - { - $this->meta = array(); - - $data = explode('*', (string)$data); - - // ...skip unilateral untagged server responses - for ($i=0, $len=count($data); $i<$len; $i++) { - if (preg_match('/^ THREAD/i', $data[$i])) { - // valid response, initialize raw_data for is_error() - $this->raw_data = ''; - $data[$i] = substr($data[$i], 7); - break; - } - - unset($data[$i]); - } - - if (empty($data)) { - return; - } - - $data = array_shift($data); - $data = trim($data); - $data = preg_replace('/[\r\n]/', '', $data); - $data = preg_replace('/\s+/', ' ', $data); - - $this->raw_data = $this->parse_thread($data); - } - - - /** - * Checks the result from IMAP command - * - * @return bool True if the result is an error, False otherwise - */ - public function is_error() - { - return $this->raw_data === null ? true : false; - } - - - /** - * Checks if the result is empty - * - * @return bool True if the result is empty, False otherwise - */ - public function is_empty() - { - return empty($this->raw_data) ? true : false; - } - - - /** - * Returns number of elements (threads) in the result - * - * @return int Number of elements - */ - public function count() - { - if ($this->meta['count'] !== null) - return $this->meta['count']; - - if (empty($this->raw_data)) { - $this->meta['count'] = 0; - } - else { - $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT); - } - - if (!$this->meta['count']) - $this->meta['messages'] = 0; - - return $this->meta['count']; - } - - - /** - * Returns number of all messages in the result - * - * @return int Number of elements - */ - public function count_messages() - { - if ($this->meta['messages'] !== null) - return $this->meta['messages']; - - if (empty($this->raw_data)) { - $this->meta['messages'] = 0; - } - else { - $this->meta['messages'] = 1 - + substr_count($this->raw_data, self::SEPARATOR_ELEMENT) - + substr_count($this->raw_data, self::SEPARATOR_ITEM); - } - - if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1) - $this->meta['count'] = $this->meta['messages']; - - return $this->meta['messages']; - } - - - /** - * Returns maximum message identifier in the result - * - * @return int Maximum message identifier - */ - public function max() - { - if (!isset($this->meta['max'])) { - $this->meta['max'] = (int) @max($this->get()); - } - return $this->meta['max']; - } - - - /** - * Returns minimum message identifier in the result - * - * @return int Minimum message identifier - */ - public function min() - { - if (!isset($this->meta['min'])) { - $this->meta['min'] = (int) @min($this->get()); - } - return $this->meta['min']; - } - - - /** - * Slices data set. - * - * @param $offset Offset (as for PHP's array_slice()) - * @param $length Number of elements (as for PHP's array_slice()) - */ - public function slice($offset, $length) - { - $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data); - $data = array_slice($data, $offset, $length); - - $this->meta = array(); - $this->meta['count'] = count($data); - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data); - } - - - /** - * Filters data set. Removes threads not listed in $roots list. - * - * @param array $roots List of IDs of thread roots. - */ - public function filter($roots) - { - $datalen = strlen($this->raw_data); - $roots = array_flip($roots); - $result = ''; - $start = 0; - - $this->meta = array(); - $this->meta['count'] = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - // extract root message ID - if ($npos = strpos($elem, self::SEPARATOR_ITEM)) { - $root = (int) substr($elem, 0, $npos); - } - else { - $root = $elem; - } - - if (isset($roots[$root])) { - $this->meta['count']++; - $result .= self::SEPARATOR_ELEMENT . $elem; - } - } - - $this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT); - } - - - /** - * Reverts order of elements in the result - */ - public function revert() - { - $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; - - if (empty($this->raw_data)) { - return; - } - - $this->meta['pos'] = array(); - $datalen = strlen($this->raw_data); - $result = ''; - $start = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - $result = $elem . self::SEPARATOR_ELEMENT . $result; - } - - $this->raw_data = rtrim($result, self::SEPARATOR_ELEMENT); - } - - - /** - * Check if the given message ID exists in the object - * - * @param int $msgid Message ID - * @param bool $get_index When enabled element's index will be returned. - * Elements are indexed starting with 0 - * - * @return boolean True on success, False if message ID doesn't exist - */ - public function exists($msgid, $get_index = false) - { - $msgid = (int) $msgid; - $begin = implode('|', array( - '^', - preg_quote(self::SEPARATOR_ELEMENT, '/'), - preg_quote(self::SEPARATOR_LEVEL, '/'), - )); - $end = implode('|', array( - '$', - preg_quote(self::SEPARATOR_ELEMENT, '/'), - preg_quote(self::SEPARATOR_ITEM, '/'), - )); - - if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m, - $get_index ? PREG_OFFSET_CAPTURE : null) - ) { - if ($get_index) { - $idx = 0; - if ($m[0][1]) { - $idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1) - + substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1); - } - // cache position of this element, so we can use it in get_element() - $this->meta['pos'][$idx] = (int)$m[0][1]; - - return $idx; - } - return true; - } - - return false; - } - - - /** - * Return IDs of all messages in the result. Threaded data will be flattened. - * - * @return array List of message identifiers - */ - public function get() - { - if (empty($this->raw_data)) { - return array(); - } - - $regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/') - . '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') - .')/'; - - return preg_split($regexp, $this->raw_data); - } - - - /** - * Return all messages in the result. - * - * @return array List of message identifiers - */ - public function get_compressed() - { - if (empty($this->raw_data)) { - return ''; - } - - return rcube_imap_generic::compressMessageSet($this->get()); - } - - - /** - * Return result element at specified index (all messages, not roots) - * - * @param int|string $index Element's index or "FIRST" or "LAST" - * - * @return int Element value - */ - public function get_element($index) - { - $count = $this->count(); - - if (!$count) { - return null; - } - - // first element - if ($index === 0 || $index === '0' || $index === 'FIRST') { - preg_match('/^([0-9]+)/', $this->raw_data, $m); - $result = (int) $m[1]; - return $result; - } - - // last element - if ($index === 'LAST' || $index == $count-1) { - preg_match('/([0-9]+)$/', $this->raw_data, $m); - $result = (int) $m[1]; - return $result; - } - - // do we know the position of the element or the neighbour of it? - if (!empty($this->meta['pos'])) { - $element = preg_quote(self::SEPARATOR_ELEMENT, '/'); - $item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?'; - $regexp = '(' . $element . '|' . $item . ')'; - - if (isset($this->meta['pos'][$index])) { - if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index])) - $result = $m[1]; - } - else if (isset($this->meta['pos'][$index-1])) { - // get chunk of data after previous element - $data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50); - $data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position - $data = preg_replace("/^$regexp/", '', $data); // remove separator - if (preg_match('/^([0-9]+)/', $data, $m)) - $result = $m[1]; - } - else if (isset($this->meta['pos'][$index+1])) { - // get chunk of data before next element - $pos = max(0, $this->meta['pos'][$index+1] - 50); - $len = min(50, $this->meta['pos'][$index+1]); - $data = substr($this->raw_data, $pos, $len); - $data = preg_replace("/$regexp\$/", '', $data); // remove separator - - if (preg_match('/([0-9]+)$/', $data, $m)) - $result = $m[1]; - } - - if (isset($result)) { - return (int) $result; - } - } - - // Finally use less effective method - $data = $this->get(); - - return $data[$index]; - } - - - /** - * Returns response parameters e.g. MAILBOX, ORDER - * - * @param string $param Parameter name - * - * @return array|string Response parameters or parameter value - */ - public function get_parameters($param=null) - { - $params = $this->params; - $params['MAILBOX'] = $this->mailbox; - $params['ORDER'] = $this->order; - - if ($param !== null) { - return $params[$param]; - } - - return $params; - } - - - /** - * THREAD=REFS sorting implementation (based on provided index) - * - * @param rcube_result_index $index Sorted message identifiers - */ - public function sort($index) - { - $this->sort_order = $index->get_parameters('ORDER'); - - if (empty($this->raw_data)) { - return; - } - - // when sorting search result it's good to make the index smaller - if ($index->count() != $this->count_messages()) { - $index->intersect($this->get()); - } - - $result = array_fill_keys($index->get(), null); - $datalen = strlen($this->raw_data); - $start = 0; - - // Here we're parsing raw_data twice, we want only one big array - // in memory at a time - - // Assign roots - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - $items = explode(self::SEPARATOR_ITEM, $elem); - $root = (int) array_shift($items); - - if ($root) { - $result[$root] = $root; - foreach ($items as $item) { - list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item); - $result[$id] = $root; - } - } - } - - // get only unique roots - $result = array_filter($result); // make sure there are no nulls - $result = array_unique($result); - - // Re-sort raw data - $result = array_fill_keys($result, null); - $start = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $start = $pos + 1; - - $npos = strpos($elem, self::SEPARATOR_ITEM); - $root = (int) ($npos ? substr($elem, 0, $npos) : $elem); - - $result[$root] = $elem; - } - - $this->raw_data = implode(self::SEPARATOR_ELEMENT, $result); - } - - - /** - * Returns data as tree - * - * @return array Data tree - */ - public function get_tree() - { - $datalen = strlen($this->raw_data); - $result = array(); - $start = 0; - - while (($pos = @strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) - || ($start < $datalen && ($pos = $datalen)) - ) { - $len = $pos - $start; - $elem = substr($this->raw_data, $start, $len); - $items = explode(self::SEPARATOR_ITEM, $elem); - $result[array_shift($items)] = $this->build_thread($items); - $start = $pos + 1; - } - - return $result; - } - - - /** - * Returns thread depth and children data - * - * @return array Thread data - */ - public function get_thread_data() - { - $data = $this->get_tree(); - $depth = array(); - $children = array(); - - $this->build_thread_data($data, $depth, $children); - - return array($depth, $children); - } - - - /** - * Creates 'depth' and 'children' arrays from stored thread 'tree' data. - */ - protected function build_thread_data($data, &$depth, &$children, $level = 0) - { - foreach ((array)$data as $key => $val) { - $empty = empty($val) || !is_array($val); - $children[$key] = !$empty; - $depth[$key] = $level; - if (!$empty) { - $this->build_thread_data($val, $depth, $children, $level + 1); - } - } - } - - - /** - * Converts part of the raw thread into an array - */ - protected function build_thread($items, $level = 1, &$pos = 0) - { - $result = array(); - - for ($len=count($items); $pos < $len; $pos++) { - list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]); - if ($level == $lv) { - $pos++; - $result[$id] = $this->build_thread($items, $level+1, $pos); - } - else { - $pos--; - break; - } - } - - return $result; - } - - - /** - * IMAP THREAD response parser - */ - protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0) - { - // Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about - // 7 times instead :-) See comments on http://uk2.php.net/references and this article: - // http://derickrethans.nl/files/phparch-php-variables-article.pdf - $node = ''; - if (!$end) { - $end = strlen($str); - } - - // Let's try to store data in max. compacted stracture as a string, - // arrays handling is much more expensive - // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96)) - // -- 2 - // - // -- 3 - // \-- 6 - // |-- 4 - // | \-- 23 - // | - // \-- 44 - // \-- 7 - // \-- 96 - // - // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96 - - if ($str[$begin] != '(') { - $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin); - $msg = substr($str, $begin, $stop - $begin); - if (!$msg) { - return $node; - } - - $this->meta['messages']++; - - $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg; - - if ($stop + 1 < $end) { - $node .= $this->parse_thread($str, $stop + 1, $end, $depth + 1); - } - } else { - $off = $begin; - while ($off < $end) { - $start = $off; - $off++; - $n = 1; - while ($n > 0) { - $p = strpos($str, ')', $off); - if ($p === false) { - // error, wrong structure, mismatched brackets in IMAP THREAD response - // @TODO: write error to the log or maybe set $this->raw_data = null; - return $node; - } - $p1 = strpos($str, '(', $off); - if ($p1 !== false && $p1 < $p) { - $off = $p1 + 1; - $n++; - } else { - $off = $p + 1; - $n--; - } - } - - $thread = $this->parse_thread($str, $start + 1, $off - 1, $depth); - if ($thread) { - if (!$depth) { - if ($node) { - $node .= self::SEPARATOR_ELEMENT; - } - } - $node .= $thread; - } - } - } - - return $node; - } -} diff --git a/program/include/rcube_session.php b/program/include/rcube_session.php deleted file mode 100644 index 6192466cd..000000000 --- a/program/include/rcube_session.php +++ /dev/null @@ -1,649 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_session.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2011, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Provide database supported session management | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - -/** - * Class to provide database supported session storage - * - * @package Core - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - */ -class rcube_session -{ - private $db; - private $ip; - private $start; - private $changed; - private $unsets = array(); - private $gc_handlers = array(); - private $cookiename = 'roundcube_sessauth'; - private $vars; - private $key; - private $now; - private $secret = ''; - private $ip_check = false; - private $logging = false; - private $keep_alive = 0; - private $memcache; - - /** - * Default constructor - */ - public function __construct($db, $config) - { - $this->db = $db; - $this->start = microtime(true); - $this->ip = $_SERVER['REMOTE_ADDR']; - $this->logging = $config->get('log_session', false); - - $lifetime = $config->get('session_lifetime', 1) * 60; - $this->set_lifetime($lifetime); - - // use memcache backend - if ($config->get('session_storage', 'db') == 'memcache') { - $this->memcache = rcube::get_instance()->get_memcache(); - - // set custom functions for PHP session management if memcache is available - if ($this->memcache) { - session_set_save_handler( - array($this, 'open'), - array($this, 'close'), - array($this, 'mc_read'), - array($this, 'mc_write'), - array($this, 'mc_destroy'), - array($this, 'gc')); - } - else { - rcube::raise_error(array('code' => 604, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => "Failed to connect to memcached. Please check configuration"), - true, true); - } - } - else { - // set custom functions for PHP session management - session_set_save_handler( - array($this, 'open'), - array($this, 'close'), - array($this, 'db_read'), - array($this, 'db_write'), - array($this, 'db_destroy'), - array($this, 'db_gc')); - } - } - - - public function open($save_path, $session_name) - { - return true; - } - - - public function close() - { - return true; - } - - - /** - * Delete session data for the given key - * - * @param string Session ID - */ - public function destroy($key) - { - return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key); - } - - - /** - * Read session data from database - * - * @param string Session ID - * @return string Session vars - */ - public function db_read($key) - { - $sql_result = $this->db->query( - "SELECT vars, ip, changed FROM ".$this->db->table_name('session') - ." WHERE sess_id = ?", $key); - - if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) { - $this->changed = strtotime($sql_arr['changed']); - $this->ip = $sql_arr['ip']; - $this->vars = base64_decode($sql_arr['vars']); - $this->key = $key; - - return !empty($this->vars) ? (string) $this->vars : ''; - } - - return null; - } - - - /** - * Save session data. - * handler for session_read() - * - * @param string Session ID - * @param string Serialized session vars - * @return boolean True on success - */ - public function db_write($key, $vars) - { - $ts = microtime(true); - $now = $this->db->fromunixtime((int)$ts); - - // no session row in DB (db_read() returns false) - if (!$this->key) { - $oldvars = null; - } - // use internal data from read() for fast requests (up to 0.5 sec.) - else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) { - $oldvars = $this->vars; - } - else { // else read data again from DB - $oldvars = $this->db_read($key); - } - - if ($oldvars !== null) { - $newvars = $this->_fixvars($vars, $oldvars); - - if ($newvars !== $oldvars) { - $this->db->query( - sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?", - $this->db->table_name('session'), $now), - base64_encode($newvars), $key); - } - else if ($ts - $this->changed > $this->lifetime / 2) { - $this->db->query("UPDATE ".$this->db->table_name('session')." SET changed=$now WHERE sess_id=?", $key); - } - } - else { - $this->db->query( - sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ". - "VALUES (?, ?, ?, %s, %s)", - $this->db->table_name('session'), $now, $now), - $key, base64_encode($vars), (string)$this->ip); - } - - return true; - } - - - /** - * Merge vars with old vars and apply unsets - */ - private function _fixvars($vars, $oldvars) - { - if ($oldvars !== null) { - $a_oldvars = $this->unserialize($oldvars); - if (is_array($a_oldvars)) { - foreach ((array)$this->unsets as $k) - unset($a_oldvars[$k]); - - $newvars = $this->serialize(array_merge( - (array)$a_oldvars, (array)$this->unserialize($vars))); - } - else - $newvars = $vars; - } - - $this->unsets = array(); - return $newvars; - } - - - /** - * Handler for session_destroy() - * - * @param string Session ID - * - * @return boolean True on success - */ - public function db_destroy($key) - { - if ($key) { - $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?", $this->db->table_name('session')), $key); - } - - return true; - } - - - /** - * Garbage collecting function - * - * @param string Session lifetime in seconds - * @return boolean True on success - */ - public function db_gc($maxlifetime) - { - // just delete all expired sessions - $this->db->query( - sprintf("DELETE FROM %s WHERE changed < %s", - $this->db->table_name('session'), $this->db->fromunixtime(time() - $maxlifetime))); - - $this->gc(); - - return true; - } - - - /** - * Read session data from memcache - * - * @param string Session ID - * @return string Session vars - */ - public function mc_read($key) - { - if ($value = $this->memcache->get($key)) { - $arr = unserialize($value); - $this->changed = $arr['changed']; - $this->ip = $arr['ip']; - $this->vars = $arr['vars']; - $this->key = $key; - - return !empty($this->vars) ? (string) $this->vars : ''; - } - - return null; - } - - - /** - * Save session data. - * handler for session_read() - * - * @param string Session ID - * @param string Serialized session vars - * @return boolean True on success - */ - public function mc_write($key, $vars) - { - $ts = microtime(true); - - // no session data in cache (mc_read() returns false) - if (!$this->key) - $oldvars = null; - // use internal data for fast requests (up to 0.5 sec.) - else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) - $oldvars = $this->vars; - else // else read data again - $oldvars = $this->mc_read($key); - - $newvars = $oldvars !== null ? $this->_fixvars($vars, $oldvars) : $vars; - - if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2) - return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)), MEMCACHE_COMPRESSED, $this->lifetime); - - return true; - } - - - /** - * Handler for session_destroy() with memcache backend - * - * @param string Session ID - * - * @return boolean True on success - */ - public function mc_destroy($key) - { - if ($key) { - // #1488592: use 2nd argument - $this->memcache->delete($key, 0); - } - - return true; - } - - - /** - * Execute registered garbage collector routines - */ - public function gc() - { - foreach ($this->gc_handlers as $fct) { - call_user_func($fct); - } - } - - - /** - * 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; - } - - - /** - * Generate and set new session id - * - * @param boolean $destroy If enabled the current session will be destroyed - */ - public function regenerate_id($destroy=true) - { - session_regenerate_id($destroy); - - $this->vars = null; - $this->key = session_id(); - - return true; - } - - - /** - * Unset a session variable - * - * @param string Varibale name - * @return boolean True on success - */ - public function remove($var=null) - { - if (empty($var)) - return $this->destroy(session_id()); - - $this->unsets[] = $var; - unset($_SESSION[$var]); - - return true; - } - - - /** - * Kill this session - */ - public function kill() - { - $this->vars = null; - $this->ip = $_SERVER['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() - { - if ($this->key && $this->memcache) - $data = $this->mc_read($this->key); - else if ($this->key) - $data = $this->db_read($this->key); - - if ($data) - session_decode($data); - } - - - /** - * Serialize session data - */ - private function serialize($vars) - { - $data = ''; - if (is_array($vars)) - foreach ($vars as $var=>$value) - $data .= $var.'|'.serialize($value); - else - $data = 'b:0;'; - return $data; - } - - - /** - * Unserialize session data - * http://www.php.net/manual/en/function.session-decode.php#56106 - */ - private function unserialize($str) - { - $str = (string)$str; - $endptr = strlen($str); - $p = 0; - - $serialized = ''; - $items = 0; - $level = 0; - - while ($p < $endptr) { - $q = $p; - while ($str[$q] != '|') - if (++$q >= $endptr) break 2; - - if ($str[$p] == '!') { - $p++; - $has_value = false; - } else { - $has_value = true; - } - - $name = substr($str, $p, $q - $p); - $q++; - - $serialized .= 's:' . strlen($name) . ':"' . $name . '";'; - - if ($has_value) { - for (;;) { - $p = $q; - switch (strtolower($str[$q])) { - case 'n': /* null */ - case 'b': /* boolean */ - case 'i': /* integer */ - case 'd': /* decimal */ - do $q++; - while ( ($q < $endptr) && ($str[$q] != ';') ); - $q++; - $serialized .= substr($str, $p, $q - $p); - if ($level == 0) break 2; - break; - case 'r': /* reference */ - $q+= 2; - for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q]; - $q++; - $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */ - if ($level == 0) break 2; - break; - case 's': /* string */ - $q+=2; - for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q]; - $q+=2; - $q+= (int)$length + 2; - $serialized .= substr($str, $p, $q - $p); - if ($level == 0) break 2; - break; - case 'a': /* array */ - case 'o': /* object */ - do $q++; - while ( ($q < $endptr) && ($str[$q] != '{') ); - $q++; - $level++; - $serialized .= substr($str, $p, $q - $p); - break; - case '}': /* end of array|object */ - $q++; - $serialized .= substr($str, $p, $q - $p); - if (--$level == 0) break 2; - break; - default: - return false; - } - } - } else { - $serialized .= 'N;'; - $q += 2; - } - $items++; - $p = $q; - } - - return unserialize( 'a:' . $items . ':{' . $serialized . '}' ); - } - - - /** - * Setter for session lifetime - */ - public function set_lifetime($lifetime) - { - $this->lifetime = max(120, $lifetime); - - // valid time range is now - 1/2 lifetime to now + 1/2 lifetime - $now = time(); - $this->now = $now - ($now % ($this->lifetime / 2)); - } - - /** - * Setter for keep_alive interval - */ - public function set_keep_alive($keep_alive) - { - $this->keep_alive = $keep_alive; - - if ($this->lifetime < $keep_alive) - $this->set_lifetime($keep_alive + 30); - } - - /** - * Getter for keep_alive interval - */ - public function get_keep_alive() - { - return $this->keep_alive; - } - - /** - * Getter for remote IP saved with this session - */ - public function get_ip() - { - return $this->ip; - } - - /** - * Setter for cookie encryption secret - */ - function set_secret($secret) - { - $this->secret = $secret; - } - - - /** - * Enable/disable IP check - */ - function set_ip_check($check) - { - $this->ip_check = $check; - } - - - /** - * Setter for the cookie name used for session cookie - */ - function set_cookiename($cookiename) - { - if ($cookiename) - $this->cookiename = $cookiename; - } - - - /** - * Check session authentication cookie - * - * @return boolean True if valid, False if not - */ - function check_auth() - { - $this->cookie = $_COOKIE[$this->cookiename]; - $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true; - - if (!$result) - $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']); - - if ($result && $this->_mkcookie($this->now) != $this->cookie) { - $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now)); - $result = false; - - // Check if using id from a previous time slot - for ($i = 1; $i <= 2; $i++) { - $prev = $this->now - ($this->lifetime / 2) * $i; - if ($this->_mkcookie($prev) == $this->cookie) { - $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie); - $this->set_auth_cookie(); - $result = true; - } - } - } - - if (!$result) - $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev)); - - return $result; - } - - - /** - * Set session authentication cookie - */ - function set_auth_cookie() - { - $this->cookie = $this->_mkcookie($this->now); - rcube_utils::setcookie($this->cookiename, $this->cookie, 0); - $_COOKIE[$this->cookiename] = $this->cookie; - } - - - /** - * Create session cookie from session data - * - * @param int Time slot to use - */ - function _mkcookie($timeslot) - { - $auth_string = "$this->key,$this->secret,$timeslot"; - return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string)); - } - - /** - * Writes debug information to the log - */ - function log($line) - { - if ($this->logging) - rcube::write_log('session', $line); - } - -} diff --git a/program/include/rcube_shared.inc b/program/include/rcube_shared.inc deleted file mode 100644 index 4577c6df5..000000000 --- a/program/include/rcube_shared.inc +++ /dev/null @@ -1,474 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_shared.inc | - | | - | This file is part of the Roundcube PHP suite | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | CONTENTS: | - | Shared functions used by Roundcube Framework | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Roundcube shared functions - * - * @package Core - */ - - -/** - * Similar function as in_array() but case-insensitive - * - * @param string $needle Needle value - * @param array $heystack Array to search in - * - * @return boolean True if found, False if not - */ -function in_array_nocase($needle, $haystack) -{ - $needle = mb_strtolower($needle); - foreach ((array)$haystack as $value) { - if ($needle === mb_strtolower($value)) { - return true; - } - } - - return false; -} - - -/** - * Find out if the string content means true or false - * - * @param string $str Input value - * - * @return boolean Boolean value - */ -function get_boolean($str) -{ - $str = strtolower($str); - - return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true); -} - - -/** - * Parse a human readable string for a number of bytes. - * - * @param string $str Input string - * - * @return float Number of bytes - */ -function parse_bytes($str) -{ - if (is_numeric($str)) { - return floatval($str); - } - - if (preg_match('/([0-9\.]+)\s*([a-z]*)/i', $str, $regs)) { - $bytes = floatval($regs[1]); - switch (strtolower($regs[2])) { - case 'g': - case 'gb': - $bytes *= 1073741824; - break; - case 'm': - case 'mb': - $bytes *= 1048576; - break; - case 'k': - case 'kb': - $bytes *= 1024; - break; - } - } - - return floatval($bytes); -} - - -/** - * Make sure the string ends with a slash - */ -function slashify($str) -{ - return unslashify($str).'/'; -} - - -/** - * Remove slashes at the end of the string - */ -function unslashify($str) -{ - return preg_replace('/\/+$/', '', $str); -} - - -/** - * Delete all files within a folder - * - * @param string Path to directory - * - * @return boolean True on success, False if directory was not found - */ -function clear_directory($dir_path) -{ - $dir = @opendir($dir_path); - if (!$dir) { - return false; - } - - while ($file = readdir($dir)) { - if (strlen($file) > 2) { - unlink("$dir_path/$file"); - } - } - - closedir($dir); - - return true; -} - - -/** - * Returns number of seconds for a specified offset string. - * - * @param string $str String representation of the offset (e.g. 20min, 5h, 2days, 1week) - * - * @return int Number of seconds - */ -function get_offset_sec($str) -{ - if (preg_match('/^([0-9]+)\s*([smhdw])/i', $str, $regs)) { - $amount = (int) $regs[1]; - $unit = strtolower($regs[2]); - } - else { - $amount = (int) $str; - $unit = 's'; - } - - switch ($unit) { - case 'w': - $amount *= 7; - case 'd': - $amount *= 24; - case 'h': - $amount *= 60; - case 'm': - $amount *= 60; - } - - return $amount; -} - - -/** - * Create a unix timestamp with a specified offset from now. - * - * @param string $offset_str String representation of the offset (e.g. 20min, 5h, 2days) - * @param int $factor Factor to multiply with the offset - * - * @return int Unix timestamp - */ -function get_offset_time($offset_str, $factor=1) -{ - return time() + get_offset_sec($offset_str) * $factor; -} - - -/** - * Truncate string if it is longer than the allowed length. - * Replace the middle or the ending part of a string with a placeholder. - * - * @param string $str Input string - * @param int $maxlength Max. length - * @param string $placeholder Replace removed chars with this - * @param bool $ending Set to True if string should be truncated from the end - * - * @return string Abbreviated string - */ -function abbreviate_string($str, $maxlength, $placeholder='...', $ending=false) -{ - $length = mb_strlen($str); - - if ($length > $maxlength) { - if ($ending) { - return mb_substr($str, 0, $maxlength) . $placeholder; - } - - $placeholder_length = mb_strlen($placeholder); - $first_part_length = floor(($maxlength - $placeholder_length)/2); - $second_starting_location = $length - $maxlength + $first_part_length + $placeholder_length; - - $str = mb_substr($str, 0, $first_part_length) . $placeholder . mb_substr($str, $second_starting_location); - } - - return $str; -} - - -/** - * Get all keys from array (recursive). - * - * @param array $array Input array - * - * @return array List of array keys - */ -function array_keys_recursive($array) -{ - $keys = array(); - - if (!empty($array) && is_array($array)) { - foreach ($array as $key => $child) { - $keys[] = $key; - foreach (array_keys_recursive($child) as $val) { - $keys[] = $val; - } - } - } - - return $keys; -} - - -/** - * Remove all non-ascii and non-word chars except ., -, _ - */ -function asciiwords($str, $css_id = false, $replace_with = '') -{ - $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : ''); - return preg_replace("/[^$allowed]/i", $replace_with, $str); -} - - -/** - * Check if a string contains only ascii characters - * - * @param string $str String to check - * @param bool $control_chars Includes control characters - * - * @return bool - */ -function is_ascii($str, $control_chars = true) -{ - $regexp = $control_chars ? '/[^\x00-\x7F]/' : '/[^\x20-\x7E]/'; - return preg_match($regexp, $str) ? false : true; -} - - -/** - * Remove single and double quotes from a given string - * - * @param string Input value - * - * @return string Dequoted string - */ -function strip_quotes($str) -{ - return str_replace(array("'", '"'), '', $str); -} - - -/** - * Remove new lines characters from given string - * - * @param string $str Input value - * - * @return string Stripped string - */ -function strip_newlines($str) -{ - return preg_replace('/[\r\n]/', '', $str); -} - - -/** - * Compose a valid representation of name and e-mail address - * - * @param string $email E-mail address - * @param string $name Person name - * - * @return string Formatted string - */ -function format_email_recipient($email, $name = '') -{ - $email = trim($email); - - if ($name && $name != $email) { - // Special chars as defined by RFC 822 need to in quoted string (or escaped). - if (preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name)) { - $name = '"'.addcslashes($name, '"').'"'; - } - - return "$name <$email>"; - } - - return $email; -} - - -/** - * 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; -} - - -/** - * mbstring replacement functions - */ -if (!extension_loaded('mbstring')) -{ - function mb_strlen($str) - { - return strlen($str); - } - - function mb_strtolower($str) - { - return strtolower($str); - } - - function mb_strtoupper($str) - { - return strtoupper($str); - } - - function mb_substr($str, $start, $len=null) - { - return substr($str, $start, $len); - } - - function mb_strpos($haystack, $needle, $offset=0) - { - return strpos($haystack, $needle, $offset); - } - - function mb_strrpos($haystack, $needle, $offset=0) - { - return strrpos($haystack, $needle, $offset); - } -} - -/** - * intl replacement functions - */ - -if (!function_exists('idn_to_utf8')) -{ - function idn_to_utf8($domain, $flags=null) - { - static $idn, $loaded; - - if (!$loaded) { - $idn = new Net_IDNA2(); - $loaded = true; - } - - if ($idn && $domain && preg_match('/(^|\.)xn--/i', $domain)) { - try { - $domain = $idn->decode($domain); - } - catch (Exception $e) { - } - } - return $domain; - } -} - -if (!function_exists('idn_to_ascii')) -{ - function idn_to_ascii($domain, $flags=null) - { - static $idn, $loaded; - - if (!$loaded) { - $idn = new Net_IDNA2(); - $loaded = true; - } - - if ($idn && $domain && preg_match('/[^\x20-\x7E]/', $domain)) { - try { - $domain = $idn->encode($domain); - } - catch (Exception $e) { - } - } - return $domain; - } -} - -/** - * Use PHP5 autoload for dynamic class loading - * - * @todo Make Zend, PEAR etc play with this - * @todo Make our classes conform to a more straight forward CS. - */ -function rcube_autoload($classname) -{ - $filename = preg_replace( - array( - '/Mail_(.+)/', - '/Net_(.+)/', - '/Auth_(.+)/', - '/^html_.+/', - '/^utf8$/', - ), - array( - 'Mail/\\1', - 'Net/\\1', - 'Auth/\\1', - 'html', - 'utf8.class', - ), - $classname - ); - - if ($fp = @fopen("$filename.php", 'r', true)) { - fclose($fp); - include_once "$filename.php"; - return true; - } - - return false; -} - -/** - * Local callback function for PEAR errors - */ -function rcube_pear_error($err) -{ - error_log(sprintf("%s (%s): %s", - $err->getMessage(), - $err->getCode(), - $err->getUserinfo()), 0); -} diff --git a/program/include/rcube_smtp.php b/program/include/rcube_smtp.php deleted file mode 100644 index b28be5206..000000000 --- a/program/include/rcube_smtp.php +++ /dev/null @@ -1,469 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_smtp.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2010, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Provide SMTP functionality using socket connections | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - -// define headers delimiter -define('SMTP_MIME_CRLF', "\r\n"); - -/** - * Class to provide SMTP functionality using PEAR Net_SMTP - * - * @package Mail - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - */ -class rcube_smtp -{ - - private $conn = null; - private $response; - private $error; - - - /** - * SMTP Connection and authentication - * - * @param string Server host - * @param string Server port - * @param string User name - * @param string Password - * - * @return bool Returns 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; - - // let plugins alter smtp connection config - $CONFIG = $rcube->plugins->exec_hook('smtp_connect', array( - 'smtp_server' => $host ? $host : $rcube->config->get('smtp_server'), - 'smtp_port' => $port ? $port : $rcube->config->get('smtp_port', 25), - 'smtp_user' => $user ? $user : $rcube->config->get('smtp_user'), - 'smtp_pass' => $pass ? $pass : $rcube->config->get('smtp_pass'), - '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_auth_callbacks' => array(), - )); - - $smtp_host = rcube_utils::parse_host($CONFIG['smtp_server']); - // when called from Installer it's possible to have empty $smtp_host here - if (!$smtp_host) $smtp_host = 'localhost'; - $smtp_port = is_numeric($CONFIG['smtp_port']) ? $CONFIG['smtp_port'] : 25; - $smtp_host_url = parse_url($smtp_host); - - // overwrite port - if (isset($smtp_host_url['host']) && isset($smtp_host_url['port'])) - { - $smtp_host = $smtp_host_url['host']; - $smtp_port = $smtp_host_url['port']; - } - - // re-write smtp host - if (isset($smtp_host_url['host']) && isset($smtp_host_url['scheme'])) - $smtp_host = sprintf('%s://%s', $smtp_host_url['scheme'], $smtp_host_url['host']); - - // remove TLS prefix and set flag for use in Net_SMTP::auth() - if (preg_match('#^tls://#i', $smtp_host)) { - $smtp_host = preg_replace('#^tls://#i', '', $smtp_host); - $use_tls = true; - } - - if (!empty($CONFIG['smtp_helo_host'])) - $helo_host = $CONFIG['smtp_helo_host']; - else if (!empty($_SERVER['SERVER_NAME'])) - $helo_host = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']); - else - $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); - - if ($rcube->config->get('smtp_debug')) - $this->conn->setDebug(true, array($this, 'debug_handler')); - - // 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'], - isset($callback['prepend']) ? $callback['prepend'] : true); - } - } - - // try to connect to server and exit on failure - $result = $this->conn->connect($smtp_timeout); - - if (PEAR::isError($result)) { - $this->response[] = "Connection failed: ".$result->getMessage(); - $this->error = array('label' => 'smtpconnerror', 'vars' => array('code' => $this->conn->_code)); - $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); - } - - $smtp_user = str_replace('%u', $_SESSION['username'], $CONFIG['smtp_user']); - $smtp_pass = str_replace('%p', $rcube->decrypt($_SESSION['password']), $CONFIG['smtp_pass']); - $smtp_auth_type = empty($CONFIG['smtp_auth_type']) ? NULL : $CONFIG['smtp_auth_type']; - - 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) - { - // 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, $use_tls, $smtp_authz); - - if (PEAR::isError($result)) - { - $this->error = array('label' => 'smtpautherror', 'vars' => array('code' => $this->conn->_code)); - $this->response[] .= 'Authentication failure: ' . $result->getMessage() . ' (Code: ' . $result->getCode() . ')'; - $this->reset(); - $this->disconnect(); - return false; - } - } - - return true; - } - - - /** - * Function for sending mail - * - * @param string Sender e-Mail address - * - * @param mixed Either a comma-seperated 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 Returns true on success, or false on error - */ - public function send_mail($from, $recipients, &$headers, &$body, $opts=null) - { - if (!is_object($this->conn)) - return false; - - // prepare message headers as string - 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; - else - { - $this->reset(); - $this->response[] = "Invalid message headers"; - return false; - } - - // exit if no from address is given - if (!isset($from)) - { - $this->reset(); - $this->response[] = "No From address has been provided"; - return false; - } - - // RFC3461: Delivery Status Notification - if ($opts['dsn']) { - $exts = $this->conn->getServiceExtensions(); - - if (isset($exts['DSN'])) { - $from_params = 'RET=HDRS'; - $recipient_params = 'NOTIFY=SUCCESS,FAILURE'; - } - } - - // RFC2298.3: remove envelope sender address - if (preg_match('/Content-Type: multipart\/report/', $text_headers) - && preg_match('/report-type=disposition-notification/', $text_headers) - ) { - $from = ''; - } - - // set From: address - if (PEAR::isError($this->conn->mailFrom($from, $from_params))) - { - $err = $this->conn->getResponse(); - $this->error = array('label' => 'smtpfromerror', 'vars' => array( - 'from' => $from, 'code' => $this->conn->_code, 'msg' => $err[1])); - $this->response[] = "Failed to set sender '$from'"; - $this->reset(); - return false; - } - - // prepare list of recipients - $recipients = $this->_parse_rfc822($recipients); - if (PEAR::isError($recipients)) - { - $this->error = array('label' => 'smtprecipientserror'); - $this->reset(); - return false; - } - - // set mail recipients - foreach ($recipients as $recipient) - { - if (PEAR::isError($this->conn->rcptTo($recipient, $recipient_params))) { - $err = $this->conn->getResponse(); - $this->error = array('label' => 'smtptoerror', 'vars' => array( - 'to' => $recipient, 'code' => $this->conn->_code, 'msg' => $err[1])); - $this->response[] = "Failed to add recipient '$recipient'"; - $this->reset(); - return false; - } - } - - if (is_resource($body)) - { - // file handle - $data = $body; - $text_headers = preg_replace('/[\r\n]+$/', '', $text_headers); - } else { - // Concatenate headers and body so it can be passed by reference to SMTP_CONN->data - // so preg_replace in SMTP_CONN->quotedata will store a reference instead of a copy. - // We are still forced to make another copy here for a couple ticks so we don't really - // get to save a copy in the method call. - $data = $text_headers . "\r\n" . $body; - - // unset old vars to save data and so we can pass into SMTP_CONN->data by reference. - unset($text_headers, $body); - } - - // Send the message's headers and the body as SMTP data. - if (PEAR::isError($result = $this->conn->data($data, $text_headers))) - { - $err = $this->conn->getResponse(); - if (!in_array($err[0], array(354, 250, 221))) - $msg = sprintf('[%d] %s', $err[0], $err[1]); - else - $msg = $result->getMessage(); - - $this->error = array('label' => 'smtperror', 'vars' => array('msg' => $msg)); - $this->response[] = "Failed to send data"; - $this->reset(); - return false; - } - - $this->response[] = join(': ', $this->conn->getResponse()); - return true; - } - - - /** - * Reset the global SMTP connection - * @access public - */ - public function reset() - { - if (is_object($this->conn)) - $this->conn->rset(); - } - - - /** - * Disconnect the global SMTP connection - * @access public - */ - public function disconnect() - { - if (is_object($this->conn)) { - $this->conn->disconnect(); - $this->conn = null; - } - } - - - /** - * This is our own debug handler for the SMTP connection - * @access public - */ - public function debug_handler(&$smtp, $message) - { - rcube::write_log('smtp', preg_replace('/\r\n$/', '', $message)); - } - - - /** - * Get error message - * @access public - */ - public function get_error() - { - return $this->error; - } - - - /** - * Get server response messages array - * @access public - */ - 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. - * @access private - */ - private function _prepare_headers($headers) - { - $lines = array(); - $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 = array(); - 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 array($from, join(SMTP_MIME_CRLF, $lines) . 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-seperated list of recipients - * (RFC822 compliant), or an array of recipients, - * each RFC822 valid. - * - * @return array An array of forward paths (bare addresses). - * @access private - */ - 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 = array(); - $recipients = rcube_utils::explode_quoted_string(',', $recipients); - - reset($recipients); - while (list($k, $recipient) = each($recipients)) - { - $a = rcube_utils::explode_quoted_string(' ', $recipient); - while (list($k2, $word) = each($a)) - { - if (strpos($word, "@") > 0 && $word[strlen($word)-1] != '"') - { - $word = preg_replace('/^<|>$/', '', trim($word)); - if (in_array($word, $addresses)===false) - array_push($addresses, $word); - } - } - } - - return $addresses; - } - -} diff --git a/program/include/rcube_spellchecker.php b/program/include/rcube_spellchecker.php deleted file mode 100644 index 219dca780..000000000 --- a/program/include/rcube_spellchecker.php +++ /dev/null @@ -1,620 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_spellchecker.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2011, Kolab Systems AG | - | Copyright (C) 2008-2011, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Spellchecking using different backends | - | | - +-----------------------------------------------------------------------+ - | Author: Aleksander Machniak <machniak@kolabsys.com> | - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Helper class for spellchecking with Googielspell and PSpell support. - * - * @package Core - */ -class rcube_spellchecker -{ - private $matches = array(); - private $engine; - private $lang; - private $rc; - private $error; - private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.]([^\w]|$)/'; - private $options = array(); - private $dict; - private $have_dict; - - - // default settings - const GOOGLE_HOST = 'ssl://www.google.com'; - const GOOGLE_PORT = 443; - const MAX_SUGGESTIONS = 10; - - - /** - * Constructor - * - * @param string $lang Language code - */ - function __construct($lang = 'en') - { - $this->rc = rcube::get_instance(); - $this->engine = $this->rc->config->get('spellcheck_engine', 'googie'); - $this->lang = $lang ? $lang : 'en'; - - $this->options = array( - 'ignore_syms' => $this->rc->config->get('spellcheck_ignore_syms'), - 'ignore_nums' => $this->rc->config->get('spellcheck_ignore_nums'), - 'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'), - 'dictionary' => $this->rc->config->get('spellcheck_dictionary'), - ); - } - - - /** - * Set content and check spelling - * - * @param string $text Text content for spellchecking - * @param bool $is_html Enables HTML-to-Text conversion - * - * @return bool True when no mispelling found, otherwise false - */ - function check($text, $is_html = false) - { - // convert to plain text - if ($is_html) { - $this->content = $this->html2text($text); - } - else { - $this->content = $text; - } - - if ($this->engine == 'pspell') { - $this->matches = $this->_pspell_check($this->content); - } - else { - $this->matches = $this->_googie_check($this->content); - } - - return $this->found() == 0; - } - - - /** - * Number of mispellings found (after check) - * - * @return int Number of mispellings - */ - function found() - { - return count($this->matches); - } - - - /** - * Returns suggestions for the specified word - * - * @param string $word The word - * - * @return array Suggestions list - */ - function get_suggestions($word) - { - if ($this->engine == 'pspell') { - return $this->_pspell_suggestions($word); - } - - return $this->_googie_suggestions($word); - } - - - /** - * Returns 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 ($this->engine == 'pspell') { - return $this->_pspell_words($text, $is_html); - } - - return $this->_googie_words($text, $is_html); - } - - - /** - * Returns checking result in XML (Googiespell) format - * - * @return string XML content - */ - function get_xml() - { - // send output - $out = '<?xml version="1.0" encoding="'.RCMAIL_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">'; - - foreach ($this->matches as $item) { - $out .= '<c o="'.$item[1].'" l="'.$item[2].'">'; - $out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4]; - $out .= '</c>'; - } - - $out .= '</spellresult>'; - - return $out; - } - - - /** - * Returns checking result (misspelled words with suggestions) - * - * @return array Spellchecking result. An array indexed by word. - */ - function get() - { - $result = array(); - - foreach ($this->matches as $item) { - if ($this->engine == 'pspell') { - $word = $item[0]; - } - else { - $word = mb_substr($this->content, $item[1], $item[2], RCMAIL_CHARSET); - } - $result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4]; - } - - return $result; - } - - - /** - * Returns error message - * - * @return string Error message - */ - function error() - { - return $this->error; - } - - - /** - * Checks the text using pspell - * - * @param string $text Text content for spellchecking - */ - private function _pspell_check($text) - { - // init spellchecker - $this->_pspell_init(); - - if (!$this->plink) { - return array(); - } - - // tokenize - $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - - $diff = 0; - $matches = array(); - - foreach ($text as $w) { - $word = trim($w[0]); - $pos = $w[1] - $diff; - $len = mb_strlen($word); - - // skip exceptions - if ($this->is_exception($word)) { - } - else if (!pspell_check($this->plink, $word)) { - $suggestions = pspell_suggest($this->plink, $word); - - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { - $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); - } - - $matches[] = array($word, $pos, $len, null, $suggestions); - } - - $diff += (strlen($word) - $len); - } - - return $matches; - } - - - /** - * Returns the misspelled words - */ - private function _pspell_words($text = null, $is_html=false) - { - $result = array(); - - if ($text) { - // init spellchecker - $this->_pspell_init(); - - if (!$this->plink) { - return array(); - } - - // With PSpell we don't need to get suggestions to return misspelled words - if ($is_html) { - $text = $this->html2text($text); - } - - $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - - foreach ($text as $w) { - $word = trim($w[0]); - - // skip exceptions - if ($this->is_exception($word)) { - continue; - } - - if (!pspell_check($this->plink, $word)) { - $result[] = $word; - } - } - - return $result; - } - - foreach ($this->matches as $m) { - $result[] = $m[0]; - } - - return $result; - } - - - /** - * Returns suggestions for misspelled word - */ - private function _pspell_suggestions($word) - { - // init spellchecker - $this->_pspell_init(); - - if (!$this->plink) { - return array(); - } - - $suggestions = pspell_suggest($this->plink, $word); - - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) - $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS); - - return is_array($suggestions) ? $suggestions : array(); - } - - - /** - * Initializes PSpell dictionary - */ - private function _pspell_init() - { - if (!$this->plink) { - if (!extension_loaded('pspell')) { - $this->error = "Pspell extension not available"; - rcube::raise_error(array( - 'code' => 500, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => $this->error), true, false); - - return; - } - - $this->plink = pspell_new($this->lang, null, null, RCMAIL_CHARSET, PSPELL_FAST); - } - - if (!$this->plink) { - $this->error = "Unable to load Pspell engine for selected language"; - } - } - - - private function _googie_check($text) - { - // spell check uri is configured - $url = $this->rc->config->get('spellcheck_uri'); - - if ($url) { - $a_uri = parse_url($url); - $ssl = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl'); - $port = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80); - $host = ($ssl ? 'ssl://' : '') . $a_uri['host']; - $path = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang; - } - else { - $host = self::GOOGLE_HOST; - $port = self::GOOGLE_PORT; - $path = '/tbproxy/spell?lang=' . $this->lang; - } - - // Google has some problem with spaces, use \n instead - $gtext = str_replace(' ', "\n", $text); - - $gtext = '<?xml version="1.0" encoding="utf-8" ?>' - .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">' - .'<text>' . $gtext . '</text>' - .'</spellrequest>'; - - $store = ''; - if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) { - $out = "POST $path HTTP/1.0\r\n"; - $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n"; - $out .= "Content-Length: " . strlen($gtext) . "\r\n"; - $out .= "Content-Type: application/x-www-form-urlencoded\r\n"; - $out .= "Connection: Close\r\n\r\n"; - $out .= $gtext; - fwrite($fp, $out); - - while (!feof($fp)) - $store .= fgets($fp, 128); - fclose($fp); - } - - if (!$store) { - $this->error = "Empty result from spelling engine"; - } - - preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER); - - // skip exceptions (if appropriate options are enabled) - if (!empty($this->options['ignore_syms']) || !empty($this->options['ignore_nums']) - || !empty($this->options['ignore_caps']) || !empty($this->options['dictionary']) - ) { - foreach ($matches as $idx => $m) { - $word = mb_substr($text, $m[1], $m[2], RCMAIL_CHARSET); - // skip exceptions - if ($this->is_exception($word)) { - unset($matches[$idx]); - } - } - } - - return $matches; - } - - - private function _googie_words($text = null, $is_html=false) - { - if ($text) { - if ($is_html) { - $text = $this->html2text($text); - } - - $matches = $this->_googie_check($text); - } - else { - $matches = $this->matches; - $text = $this->content; - } - - $result = array(); - - foreach ($matches as $m) { - $result[] = mb_substr($text, $m[1], $m[2], RCMAIL_CHARSET); - } - - return $result; - } - - - private function _googie_suggestions($word) - { - if ($word) { - $matches = $this->_googie_check($word); - } - else { - $matches = $this->matches; - } - - if ($matches[0][4]) { - $suggestions = explode("\t", $matches[0][4]); - if (sizeof($suggestions) > self::MAX_SUGGESTIONS) { - $suggestions = array_slice($suggestions, 0, MAX_SUGGESTIONS); - } - - return $suggestions; - } - - return array(); - } - - - private function html2text($text) - { - $h2t = new html2text($text, false, true, 0); - return $h2t->get_text(); - } - - - /** - * Check if the specified word is an exception accoring to - * spellcheck options. - * - * @param string $word The word - * - * @return bool True if the word is an exception, False otherwise - */ - public function is_exception($word) - { - // Contain only symbols (e.g. "+9,0", "2:2") - if (!$word || preg_match('/^[0-9@#$%^&_+~*=:;?!,.-]+$/', $word)) - return true; - - // Contain symbols (e.g. "g@@gle"), all symbols excluding separators - if (!empty($this->options['ignore_syms']) && preg_match('/[@#$%^&_+~*=-]/', $word)) - return true; - - // Contain numbers (e.g. "g00g13") - if (!empty($this->options['ignore_nums']) && preg_match('/[0-9]/', $word)) - return true; - - // Blocked caps (e.g. "GOOGLE") - if (!empty($this->options['ignore_caps']) && $word == mb_strtoupper($word)) - return true; - - // Use exceptions from dictionary - if (!empty($this->options['dictionary'])) { - $this->load_dict(); - - // @TODO: should dictionary be case-insensitive? - if (!empty($this->dict) && in_array($word, $this->dict)) - return true; - } - - return false; - } - - - /** - * Add a word to dictionary - * - * @param string $word The word to add - */ - public function add_word($word) - { - $this->load_dict(); - - foreach (explode(' ', $word) as $word) { - // sanity check - if (strlen($word) < 512) { - $this->dict[] = $word; - $valid = true; - } - } - - if ($valid) { - $this->dict = array_unique($this->dict); - $this->update_dict(); - } - } - - - /** - * Remove a word from dictionary - * - * @param string $word The word to remove - */ - public function remove_word($word) - { - $this->load_dict(); - - if (($key = array_search($word, $this->dict)) !== false) { - unset($this->dict[$key]); - $this->update_dict(); - } - } - - - /** - * Update dictionary row in DB - */ - private function update_dict() - { - if (strcasecmp($this->options['dictionary'], 'shared') != 0) { - $userid = $this->rc->get_user_id(); - } - - $plugin = $this->rc->plugins->exec_hook('spell_dictionary_save', array( - 'userid' => $userid, 'language' => $this->lang, 'dictionary' => $this->dict)); - - if (!empty($plugin['abort'])) { - return; - } - - if ($this->have_dict) { - if (!empty($this->dict)) { - $this->rc->db->query( - "UPDATE ".$this->rc->db->table_name('dictionary') - ." SET data = ?" - ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") - ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", - implode(' ', $plugin['dictionary']), $plugin['language']); - } - // don't store empty dict - else { - $this->rc->db->query( - "DELETE FROM " . $this->rc->db->table_name('dictionary') - ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") - ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", - $plugin['language']); - } - } - else if (!empty($this->dict)) { - $this->rc->db->query( - "INSERT INTO " .$this->rc->db->table_name('dictionary') - ." (user_id, " . $this->rc->db->quoteIdentifier('language') . ", data) VALUES (?, ?, ?)", - $plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary'])); - } - } - - - /** - * Get dictionary from DB - */ - private function load_dict() - { - if (is_array($this->dict)) { - return $this->dict; - } - - if (strcasecmp($this->options['dictionary'], 'shared') != 0) { - $userid = $this->rc->get_user_id(); - } - - $plugin = $this->rc->plugins->exec_hook('spell_dictionary_get', array( - 'userid' => $userid, 'language' => $this->lang, 'dictionary' => array())); - - if (empty($plugin['abort'])) { - $dict = array(); - $this->rc->db->query( - "SELECT data FROM ".$this->rc->db->table_name('dictionary') - ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL") - ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", - $plugin['language']); - - if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) { - $this->have_dict = true; - if (!empty($sql_arr['data'])) { - $dict = explode(' ', $sql_arr['data']); - } - } - - $plugin['dictionary'] = array_merge((array)$plugin['dictionary'], $dict); - } - - if (!empty($plugin['dictionary']) && is_array($plugin['dictionary'])) { - $this->dict = $plugin['dictionary']; - } - else { - $this->dict = array(); - } - - return $this->dict; - } - -} diff --git a/program/include/rcube_storage.php b/program/include/rcube_storage.php deleted file mode 100644 index 933ebcc25..000000000 --- a/program/include/rcube_storage.php +++ /dev/null @@ -1,991 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_storage.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | - | Copyright (C) 2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Mail Storage Engine | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Abstract class for accessing mail messages storage server - * - * @package Mail - * @author Thomas Bruederli <roundcube@gmail.com> - * @author Aleksander Machniak <alec@alec.pl> - * @version 2.0 - */ -abstract class rcube_storage -{ - /** - * Instance of connection object e.g. rcube_imap_generic - * - * @var mixed - */ - public $conn; - - protected $folder = 'INBOX'; - protected $default_charset = 'ISO-8859-1'; - protected $default_folders = array('INBOX'); - protected $search_set; - protected $options = array('auth_method' => 'check'); - protected $page_size = 10; - protected $threading = false; - - /** - * All (additional) headers used (in any way) by Roundcube - * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST - * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders() - * - * @var array - */ - protected $all_headers = array( - 'IN-REPLY-TO', - 'BCC', - 'MESSAGE-ID', - 'CONTENT-TRANSFER-ENCODING', - 'REFERENCES', - 'X-DRAFT-INFO', - 'MAIL-FOLLOWUP-TO', - 'MAIL-REPLY-TO', - 'RETURN-PATH', - ); - - const UNKNOWN = 0; - const NOPERM = 1; - const READONLY = 2; - const TRYCREATE = 3; - const INUSE = 4; - const OVERQUOTA = 5; - const ALREADYEXISTS = 6; - const NONEXISTENT = 7; - const CONTACTADMIN = 8; - - - /** - * Connect to the server - * - * @param string $host Host to connect - * @param string $user Username for IMAP account - * @param string $pass Password for IMAP account - * @param integer $port Port to connect to - * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection - * - * @return boolean TRUE on success, FALSE on failure - */ - abstract function connect($host, $user, $pass, $port = 143, $use_ssl = null); - - - /** - * Close connection. Usually done on script shutdown - */ - abstract function close(); - - - /** - * Checks connection state. - * - * @return boolean TRUE on success, FALSE on failure - */ - abstract function is_connected(); - - - /** - * Check connection state, connect if not connected. - * - * @return bool Connection state. - */ - abstract function check_connection(); - - - /** - * Returns code of last error - * - * @return int Error code - */ - abstract function get_error_code(); - - - /** - * Returns message of last error - * - * @return string Error message - */ - abstract function get_error_str(); - - - /** - * Returns code of last command response - * - * @return int Response code (class constant) - */ - abstract function get_response_code(); - - - /** - * Set connection and class options - * - * @param array $opt Options array - */ - public function set_options($opt) - { - $this->options = array_merge($this->options, (array)$opt); - } - - - /** - * Activate/deactivate debug mode. - * - * @param boolean $dbg True if conversation with the server should be logged - */ - abstract function set_debug($dbg = true); - - - /** - * Set default message charset. - * - * This will be used for message decoding if a charset specification is not available - * - * @param string $cs Charset string - */ - public function set_charset($cs) - { - $this->default_charset = $cs; - } - - - /** - * This list of folders will be listed above all other folders - * - * @param array $arr Indexed list of folder names - */ - public function set_default_folders($arr) - { - if (is_array($arr)) { - $this->default_folders = $arr; - - // add inbox if not included - if (!in_array('INBOX', $this->default_folders)) { - array_unshift($this->default_folders, 'INBOX'); - } - } - } - - - /** - * Set internal folder reference. - * All operations will be perfomed on this folder. - * - * @param string $folder Folder name - */ - public function set_folder($folder) - { - if ($this->folder === $folder) { - return; - } - - $this->folder = $folder; - } - - - /** - * Returns the currently used folder name - * - * @return string Name of the folder - */ - public function get_folder() - { - return $this->folder; - } - - - /** - * Set internal list page number. - * - * @param int $page Page number to list - */ - public function set_page($page) - { - $this->list_page = (int) $page; - } - - - /** - * Gets internal list page number. - * - * @return int Page number - */ - public function get_page() - { - return $this->list_page; - } - - - /** - * Set internal page size - * - * @param int $size Number of messages to display on one page - */ - public function set_pagesize($size) - { - $this->page_size = (int) $size; - } - - - /** - * Get internal page size - * - * @return int Number of messages to display on one page - */ - public function get_pagesize() - { - return $this->page_size; - } - - - /** - * Save a search result for future message listing methods. - * - * @param mixed $set Search set in driver specific format - */ - abstract function set_search_set($set); - - - /** - * Return the saved search set. - * - * @return array Search set in driver specific format, NULL if search wasn't initialized - */ - abstract function get_search_set(); - - - /** - * Returns the storage server's (IMAP) capability - * - * @param string $cap Capability name - * - * @return mixed Capability value or TRUE if supported, FALSE if not - */ - abstract function get_capability($cap); - - - /** - * Sets threading flag to the best supported THREAD algorithm. - * Enable/Disable threaded mode. - * - * @param boolean $enable TRUE to enable and FALSE - * - * @return mixed Threading algorithm or False if THREAD is not supported - */ - public function set_threading($enable = false) - { - $this->threading = false; - - if ($enable && ($caps = $this->get_capability('THREAD'))) { - $methods = array('REFS', 'REFERENCES', 'ORDEREDSUBJECT'); - $methods = array_intersect($methods, $caps); - - $this->threading = array_shift($methods); - } - - return $this->threading; - } - - - /** - * Get current threading flag. - * - * @return mixed Threading algorithm or False if THREAD is not supported or disabled - */ - public function get_threading() - { - return $this->threading; - } - - - /** - * Checks the PERMANENTFLAGS capability of the current folder - * and returns true if the given flag is supported by the server. - * - * @param string $flag Permanentflag name - * - * @return boolean True if this flag is supported - */ - abstract function check_permflag($flag); - - - /** - * Returns the delimiter that is used by the server - * for folder hierarchy separation. - * - * @return string Delimiter string - */ - abstract function get_hierarchy_delimiter(); - - - /** - * Get namespace - * - * @param string $name Namespace array index: personal, other, shared, prefix - * - * @return array Namespace data - */ - abstract function get_namespace($name = null); - - - /** - * Get messages count for a specific folder. - * - * @param string $folder Folder name - * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT] - * @param boolean $force Force reading from server and update cache - * @param boolean $status Enables storing folder status info (max UID/count), - * required for folder_status() - * - * @return int Number of messages - */ - abstract function count($folder = null, $mode = 'ALL', $force = false, $status = true); - - - /** - * Public method for listing headers. - * - * @param string $folder Folder name - * @param int $page Current page to list - * @param string $sort_field Header field to sort by - * @param string $sort_order Sort order [ASC|DESC] - * @param int $slice Number of slice items to extract from result array - * - * @return array Indexed array with message header objects - */ - abstract function list_messages($folder = null, $page = null, $sort_field = null, $sort_order = null, $slice = 0); - - - /** - * Return sorted list of message UIDs - * - * @param string $folder Folder to get index from - * @param string $sort_field Sort column - * @param string $sort_order Sort order [ASC, DESC] - * - * @return rcube_result_index|rcube_result_thread List of messages (UIDs) - */ - abstract function index($folder = null, $sort_field = null, $sort_order = null); - - - /** - * Invoke search request to the server. - * - * @param string $folder Folder name to search in - * @param string $str Search criteria - * @param string $charset Search charset - * @param string $sort_field Header field to sort by - * - * @todo: Search criteria should be provided in non-IMAP format, eg. array - */ - abstract function search($folder = null, $str = 'ALL', $charset = null, $sort_field = null); - - - /** - * Direct (real and simple) search request (without result sorting and caching). - * - * @param string $folder Folder name to search in - * @param string $str Search string - * - * @return rcube_result_index Search result (UIDs) - */ - abstract function search_once($folder = null, $str = 'ALL'); - - - /** - * Refresh saved search set - * - * @return array Current search set - */ - abstract function refresh_search(); - - - /* -------------------------------- - * messages management - * --------------------------------*/ - - /** - * Fetch message headers and body structure from the server and build - * an object structure similar to the one generated by PEAR::Mail_mimeDecode - * - * @param int $uid Message UID to fetch - * @param string $folder Folder to read from - * - * @return object rcube_message_header Message data - */ - abstract function get_message($uid, $folder = null); - - - /** - * Return message headers object of a specific message - * - * @param int $id Message sequence ID or UID - * @param string $folder Folder to read from - * @param bool $force True to skip cache - * - * @return rcube_message_header Message headers - */ - abstract function get_message_headers($uid, $folder = null, $force = false); - - - /** - * Fetch message body of a specific message from the server - * - * @param int $uid Message UID - * @param string $part Part number - * @param rcube_message_part $o_part Part object created by get_structure() - * @param mixed $print True to print part, ressource to write part contents in - * @param resource $fp File pointer to save the message part - * @param boolean $skip_charset_conv Disables charset conversion - * - * @return string Message/part body if not printed - */ - abstract function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null, $skip_charset_conv = false); - - - /** - * Fetch message body of a specific message from the server - * - * @param int $uid Message UID - * - * @return string $part Message/part body - * @see rcube_imap::get_message_part() - */ - public function get_body($uid, $part = 1) - { - $headers = $this->get_message_headers($uid); - return rcube_charset::convert($this->get_message_part($uid, $part, null), - $headers->charset ? $headers->charset : $this->default_charset); - } - - - /** - * Returns the whole message source as string (or saves to a file) - * - * @param int $uid Message UID - * @param resource $fp File pointer to save the message - * - * @return string Message source string - */ - abstract function get_raw_body($uid, $fp = null); - - - /** - * Returns the message headers as string - * - * @param int $uid Message UID - * - * @return string Message headers string - */ - abstract function get_raw_headers($uid); - - - /** - * Sends the whole message source to stdout - * - * @param int $uid Message UID - * @param bool $formatted Enables line-ending formatting - */ - abstract function print_raw_body($uid, $formatted = true); - - - /** - * Set message flag to one or several messages - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT - * @param string $folder Folder name - * @param boolean $skip_cache True to skip message cache clean up - * - * @return bool Operation status - */ - abstract function set_flag($uids, $flag, $folder = null, $skip_cache = false); - - - /** - * Remove message flag for one or several messages - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $flag Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT - * @param string $folder Folder name - * - * @return bool Operation status - * @see set_flag - */ - public function unset_flag($uids, $flag, $folder = null) - { - return $this->set_flag($uids, 'UN'.$flag, $folder); - } - - - /** - * Append a mail message (source) to a specific folder. - * - * @param string $folder Target folder - * @param string $message The message source string or filename - * @param string $headers Headers string if $message contains only the body - * @param boolean $is_file True if $message is a filename - * @param array $flags Message flags - * @param mixed $date Message internal date - * - * @return int|bool Appended message UID or True on success, False on error - */ - abstract function save_message($folder, &$message, $headers = '', $is_file = false, $flags = array(), $date = null); - - - /** - * Move message(s) from one folder to another. - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $to Target folder - * @param string $from Source folder - * - * @return boolean True on success, False on error - */ - abstract function move_message($uids, $to, $from = null); - - - /** - * Copy message(s) from one mailbox to another. - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $to Target folder - * @param string $from Source folder - * - * @return boolean True on success, False on error - */ - abstract function copy_message($uids, $to, $from = null); - - - /** - * Mark message(s) as deleted and expunge. - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $folder Source folder - * - * @return boolean True on success, False on error - */ - abstract function delete_message($uids, $folder = null); - - - /** - * Expunge message(s) and clear the cache. - * - * @param mixed $uids Message UIDs as array or comma-separated string, or '*' - * @param string $folder Folder name - * @param boolean $clear_cache False if cache should not be cleared - * - * @return boolean True on success, False on error - */ - abstract function expunge_message($uids, $folder = null, $clear_cache = true); - - - /** - * Parse message UIDs input - * - * @param mixed $uids UIDs array or comma-separated list or '*' or '1:*' - * - * @return array Two elements array with UIDs converted to list and ALL flag - */ - protected function parse_uids($uids) - { - if ($uids === '*' || $uids === '1:*') { - if (empty($this->search_set)) { - $uids = '1:*'; - $all = true; - } - // get UIDs from current search set - else { - $uids = join(',', $this->search_set->get()); - } - } - else { - if (is_array($uids)) { - $uids = join(',', $uids); - } - - if (preg_match('/[^0-9,]/', $uids)) { - $uids = ''; - } - } - - return array($uids, (bool) $all); - } - - - /* -------------------------------- - * folder managment - * --------------------------------*/ - - /** - * Get a list of subscribed folders. - * - * @param string $root Optional root folder - * @param string $name Optional name pattern - * @param string $filter Optional filter - * @param string $rights Optional ACL requirements - * @param bool $skip_sort Enable to return unsorted list (for better performance) - * - * @return array List of folders - */ - abstract function list_folders_subscribed($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false); - - - /** - * Get a list of all folders available on the server. - * - * @param string $root IMAP root dir - * @param string $name Optional name pattern - * @param mixed $filter Optional filter - * @param string $rights Optional ACL requirements - * @param bool $skip_sort Enable to return unsorted list (for better performance) - * - * @return array Indexed array with folder names - */ - abstract function list_folders($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false); - - - /** - * Subscribe to a specific folder(s) - * - * @param array $folders Folder name(s) - * - * @return boolean True on success - */ - abstract function subscribe($folders); - - - /** - * Unsubscribe folder(s) - * - * @param array $folders Folder name(s) - * - * @return boolean True on success - */ - abstract function unsubscribe($folders); - - - /** - * Create a new folder on the server. - * - * @param string $folder New folder name - * @param boolean $subscribe True if the newvfolder should be subscribed - * - * @return boolean True on success, False on error - */ - abstract function create_folder($folder, $subscribe = false); - - - /** - * Set a new name to an existing folder - * - * @param string $folder Folder to rename - * @param string $new_name New folder name - * - * @return boolean True on success, False on error - */ - abstract function rename_folder($folder, $new_name); - - - /** - * Remove a folder from the server. - * - * @param string $folder Folder name - * - * @return boolean True on success, False on error - */ - abstract function delete_folder($folder); - - - /** - * Send expunge command and clear the cache. - * - * @param string $folder Folder name - * @param boolean $clear_cache False if cache should not be cleared - * - * @return boolean True on success, False on error - */ - public function expunge_folder($folder = null, $clear_cache = true) - { - return $this->expunge_message('*', $folder, $clear_cache); - } - - - /** - * Remove all messages in a folder.. - * - * @param string $folder Folder name - * - * @return boolean True on success, False on error - */ - public function clear_folder($folder = null) - { - return $this->delete_message('*', $folder); - } - - - /** - * Checks if folder exists and is subscribed - * - * @param string $folder Folder name - * @param boolean $subscription Enable subscription checking - * - * @return boolean True if folder exists, False otherwise - */ - abstract function folder_exists($folder, $subscription = false); - - - /** - * Get folder size (size of all messages in a folder) - * - * @param string $folder Folder name - * - * @return int Folder size in bytes, False on error - */ - abstract function folder_size($folder); - - - /** - * Returns the namespace where the folder is in - * - * @param string $folder Folder name - * - * @return string One of 'personal', 'other' or 'shared' - */ - abstract function folder_namespace($folder); - - - /** - * Gets folder attributes (from LIST response, e.g. \Noselect, \Noinferiors). - * - * @param string $folder Folder name - * @param bool $force Set to True if attributes should be refreshed - * - * @return array Options list - */ - abstract function folder_attributes($folder, $force = false); - - - /** - * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT, - * PERMANENTFLAGS, UIDNEXT, UNSEEN - * - * @param string $folder Folder name - * - * @return array Data - */ - abstract function folder_data($folder); - - - /** - * Returns extended information about the folder. - * - * @param string $folder Folder name - * - * @return array Data - */ - abstract function folder_info($folder); - - - /** - * Returns current status of a folder - * - * @param string $folder Folder name - * - * @return int Folder status - */ - abstract function folder_status($folder = null); - - - /** - * Synchronizes messages cache. - * - * @param string $folder Folder name - */ - abstract function folder_sync($folder); - - - /** - * Modify folder name according to namespace. - * For output it removes prefix of the personal namespace if it's possible. - * For input it adds the prefix. Use it before creating a folder in root - * of the folders tree. - * - * @param string $folder Folder name - * @param string $mode Mode name (out/in) - * - * @return string Folder name - */ - abstract function mod_folder($folder, $mode = 'out'); - - - /** - * Create all folders specified as default - */ - public function create_default_folders() - { - // create default folders if they do not exist - foreach ($this->default_folders as $folder) { - if (!$this->folder_exists($folder)) { - $this->create_folder($folder, true); - } - else if (!$this->folder_exists($folder, true)) { - $this->subscribe($folder); - } - } - } - - - /** - * Get mailbox quota information. - * - * @return mixed Quota info or False if not supported - */ - abstract function get_quota(); - - - /* ----------------------------------------- - * ACL and METADATA methods - * ----------------------------------------*/ - - /** - * Changes the ACL on the specified folder (SETACL) - * - * @param string $folder Folder name - * @param string $user User name - * @param string $acl ACL string - * - * @return boolean True on success, False on failure - */ - abstract function set_acl($folder, $user, $acl); - - - /** - * Removes any <identifier,rights> pair for the - * specified user from the ACL for the specified - * folder (DELETEACL). - * - * @param string $folder Folder name - * @param string $user User name - * - * @return boolean True on success, False on failure - */ - abstract function delete_acl($folder, $user); - - - /** - * Returns the access control list for a folder (GETACL). - * - * @param string $folder Folder name - * - * @return array User-rights array on success, NULL on error - */ - abstract function get_acl($folder); - - - /** - * Returns information about what rights can be granted to the - * user (identifier) in the ACL for the folder (LISTRIGHTS). - * - * @param string $folder Folder name - * @param string $user User name - * - * @return array List of user rights - */ - abstract function list_rights($folder, $user); - - - /** - * Returns the set of rights that the current user has to a folder (MYRIGHTS). - * - * @param string $folder Folder name - * - * @return array MYRIGHTS response on success, NULL on error - */ - abstract function my_rights($folder); - - - /** - * Sets metadata/annotations (SETMETADATA/SETANNOTATION) - * - * @param string $folder Folder name (empty for server metadata) - * @param array $entries Entry-value array (use NULL value as NIL) - * - * @return boolean True on success, False on failure - */ - abstract function set_metadata($folder, $entries); - - - /** - * Unsets metadata/annotations (SETMETADATA/SETANNOTATION) - * - * @param string $folder Folder name (empty for server metadata) - * @param array $entries Entry names array - * - * @return boolean True on success, False on failure - */ - abstract function delete_metadata($folder, $entries); - - - /** - * Returns folder metadata/annotations (GETMETADATA/GETANNOTATION). - * - * @param string $folder Folder name (empty for server metadata) - * @param array $entries Entries - * @param array $options Command options (with MAXSIZE and DEPTH keys) - * - * @return array Metadata entry-value hash array on success, NULL on error - */ - abstract function get_metadata($folder, $entries, $options = array()); - - - /* ----------------------------------------- - * Cache related functions - * ----------------------------------------*/ - - /** - * Clears the cache. - * - * @param string $key Cache key name or pattern - * @param boolean $prefix_mode Enable it to clear all keys starting - * with prefix specified in $key - */ - abstract function clear_cache($key = null, $prefix_mode = false); - - - /** - * Returns cached value - * - * @param string $key Cache key - * - * @return mixed Cached value - */ - abstract function get_cache($key); - - - /** - * Delete outdated cache entries - */ - abstract function expunge_cache(); - -} // end class rcube_storage diff --git a/program/include/rcube_string_replacer.php b/program/include/rcube_string_replacer.php deleted file mode 100644 index edb2ac34f..000000000 --- a/program/include/rcube_string_replacer.php +++ /dev/null @@ -1,194 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_string_replacer.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2009, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | 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 Core - */ -class rcube_string_replacer -{ - public static $pattern = '/##str_replacement\[([0-9]+)\]##/'; - public $mailto_pattern; - public $link_pattern; - private $values = array(); - - - function __construct() - { - // 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%=#$@+?!&\\/_~\\[\\]{}\*-'; - - $this->link_pattern = "/([\w]+:\/\/|\W[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)($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 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 - */ - public function get_replacement($i) - { - return '##str_replacement['.$i.']##'; - } - - /** - * Callback function used to build HTML links around URL strings - * - * @param array Matches result from preg_replace_callback - * @return int Index of saved string value - */ - public function link_callback($matches) - { - $i = -1; - $scheme = strtolower($matches[1]); - - if (preg_match('!^(http|ftp|file)s?://!i', $scheme)) { - $url = $matches[1] . $matches[2]; - } - else if (preg_match('/^(\W*)(www\.)$/i', $matches[1], $m)) { - $url = $m[2] . $matches[2]; - $url_prefix = 'http://'; - $prefix = $m[1]; - } - - if ($url) { - $suffix = $this->parse_url_brackets($url); - $i = $this->add($prefix . html::a(array( - 'href' => $url_prefix . $url, - 'target' => '_blank' - ), rcmail::Q($url)) . $suffix); - } - - // Return valid link for recognized schemes, otherwise, return the unmodified string for unrecognized schemes. - return $i >= 0 ? $this->get_replacement($i) : $matches[0]; - } - - /** - * Callback function used to build mailto: links around e-mail strings - * - * @param array Matches result from preg_replace_callback - * @return int Index of saved string value - */ - public function mailto_callback($matches) - { - $href = $matches[1]; - $suffix = $this->parse_url_brackets($href); - - $i = $this->add(html::a(array( - 'href' => 'mailto:' . $href, - 'onclick' => "return ".rcmail::JS_OBJECT_NAME.".command('compose','".rcmail::JQ($href)."',this)", - ), rcmail::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 result from preg_replace_callback - * @return string Value at index $matches[1] - */ - public function replace_callback($matches) - { - return $this->values[$matches[1]]; - } - - /** - * Replace all defined (link|mailto) patterns with replacement string - * - * @param string $str Text - * - * @return string Text - */ - public function replace($str) - { - // search for patterns like links and e-mail addresses - $str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str); - $str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str); - - return $str; - } - - /** - * Replace substituted strings with original values - */ - public function resolve($str) - { - return preg_replace_callback(self::$pattern, array($this, 'replace_callback'), $str); - } - - /** - * Fixes bracket characters in URL handling - */ - public 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]" - 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; - } - -} diff --git a/program/include/rcube_user.php b/program/include/rcube_user.php deleted file mode 100644 index b92187ad4..000000000 --- a/program/include/rcube_user.php +++ /dev/null @@ -1,701 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_user.inc | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2010, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | This class represents a system user linked and provides access | - | to the related database records. | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Class representing a system user - * - * @package Core - * @author Thomas Bruederli <roundcube@gmail.com> - */ -class rcube_user -{ - public $ID; - public $data; - public $language; - - /** - * Holds database connection. - * - * @var rcube_db - */ - private $db; - - /** - * Framework object. - * - * @var rcube - */ - private $rc; - - const SEARCH_ADDRESSBOOK = 1; - const SEARCH_MAIL = 2; - - /** - * Object constructor - * - * @param int $id User id - * @param array $sql_arr SQL result set - */ - function __construct($id = null, $sql_arr = null) - { - $this->rc = rcube::get_instance(); - $this->db = $this->rc->get_dbh(); - - if ($id && !$sql_arr) { - $sql_result = $this->db->query( - "SELECT * FROM ".$this->db->table_name('users')." WHERE user_id = ?", $id); - $sql_arr = $this->db->fetch_assoc($sql_result); - } - - if (!empty($sql_arr)) { - $this->ID = $sql_arr['user_id']; - $this->data = $sql_arr; - $this->language = $sql_arr['language']; - } - } - - - /** - * Build a user name string (as e-mail address) - * - * @param string $part Username part (empty or 'local' or 'domain') - * @return string Full user name or its part - */ - function get_username($part = null) - { - if ($this->data['username']) { - list($local, $domain) = explode('@', $this->data['username']); - - // at least we should always have the local part - if ($part == 'local') { - return $local; - } - // if no domain was provided... - if (empty($domain)) { - $domain = $this->rc->config->mail_domain($this->data['mail_host']); - } - - if ($part == 'domain') { - return $domain; - } - - if (!empty($domain)) - return $local . '@' . $domain; - else - return $local; - } - - return false; - } - - - /** - * Get the preferences saved for this user - * - * @return array Hash array with prefs - */ - function get_prefs() - { - if (!empty($this->language)) - $prefs = array('language' => $this->language); - - if ($this->ID) { - // Preferences from session (write-master is unavailable) - if (!empty($_SESSION['preferences'])) { - // Check last write attempt time, try to write again (every 5 minutes) - if ($_SESSION['preferences_time'] < time() - 5 * 60) { - $saved_prefs = unserialize($_SESSION['preferences']); - $this->rc->session->remove('preferences'); - $this->rc->session->remove('preferences_time'); - $this->save_prefs($saved_prefs); - } - else { - $this->data['preferences'] = $_SESSION['preferences']; - } - } - - if ($this->data['preferences']) { - $prefs += (array)unserialize($this->data['preferences']); - } - } - - return $prefs; - } - - - /** - * Write the given user prefs to the user's record - * - * @param array $a_user_prefs User prefs to save - * @return boolean True on success, False on failure - */ - function save_prefs($a_user_prefs) - { - if (!$this->ID) - return false; - - $config = $this->rc->config; - $old_prefs = (array)$this->get_prefs(); - - // merge (partial) prefs array with existing settings - $save_prefs = $a_user_prefs + $old_prefs; - unset($save_prefs['language']); - - // don't save prefs with default values if they haven't been changed yet - foreach ($a_user_prefs as $key => $value) { - if (!isset($old_prefs[$key]) && ($value == $config->get($key))) - unset($save_prefs[$key]); - } - - $save_prefs = serialize($save_prefs); - - $this->db->query( - "UPDATE ".$this->db->table_name('users'). - " SET preferences = ?". - ", language = ?". - " WHERE user_id = ?", - $save_prefs, - $_SESSION['language'], - $this->ID); - - $this->language = $_SESSION['language']; - - // Update success - if ($this->db->affected_rows() !== false) { - $config->set_user_prefs($a_user_prefs); - $this->data['preferences'] = $save_prefs; - - if (isset($_SESSION['preferences'])) { - $this->rc->session->remove('preferences'); - $this->rc->session->remove('preferences_time'); - } - return true; - } - // Update error, but we are using replication (we have read-only DB connection) - // and we are storing session not in the SQL database - // we can store preferences in session and try to write later (see get_prefs()) - else if ($this->db->is_replicated() && $config->get('session_storage', 'db') != 'db') { - $_SESSION['preferences'] = $save_prefs; - $_SESSION['preferences_time'] = time(); - $config->set_user_prefs($a_user_prefs); - $this->data['preferences'] = $save_prefs; - } - - return false; - } - - - /** - * Get default identity of this user - * - * @param int $id Identity ID. If empty, the default identity is returned - * @return array Hash array with all cols of the identity record - */ - function get_identity($id = null) - { - $result = $this->list_identities($id ? sprintf('AND identity_id = %d', $id) : ''); - return $result[0]; - } - - - /** - * Return a list of all identities linked with this user - * - * @param string $sql_add Optional WHERE clauses - * @return array List of identities - */ - function list_identities($sql_add = '') - { - $result = array(); - - $sql_result = $this->db->query( - "SELECT * FROM ".$this->db->table_name('identities'). - " WHERE del <> 1 AND user_id = ?". - ($sql_add ? " ".$sql_add : ""). - " ORDER BY ".$this->db->quoteIdentifier('standard')." DESC, name ASC, identity_id ASC", - $this->ID); - - while ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $result[] = $sql_arr; - } - - return $result; - } - - - /** - * Update a specific identity record - * - * @param int $iid Identity ID - * @param array $data Hash array with col->value pairs to save - * @return boolean True if saved successfully, false if nothing changed - */ - function update_identity($iid, $data) - { - if (!$this->ID) - return false; - - $query_cols = $query_params = array(); - - foreach ((array)$data as $col => $value) { - $query_cols[] = $this->db->quoteIdentifier($col) . ' = ?'; - $query_params[] = $value; - } - $query_params[] = $iid; - $query_params[] = $this->ID; - - $sql = "UPDATE ".$this->db->table_name('identities'). - " SET changed = ".$this->db->now().", ".join(', ', $query_cols). - " WHERE identity_id = ?". - " AND user_id = ?". - " AND del <> 1"; - - call_user_func_array(array($this->db, 'query'), - array_merge(array($sql), $query_params)); - - return $this->db->affected_rows(); - } - - - /** - * Create a new identity record linked with this user - * - * @param array $data Hash array with col->value pairs to save - * @return int The inserted identity ID or false on error - */ - function insert_identity($data) - { - if (!$this->ID) - return false; - - unset($data['user_id']); - - $insert_cols = $insert_values = array(); - foreach ((array)$data as $col => $value) { - $insert_cols[] = $this->db->quoteIdentifier($col); - $insert_values[] = $value; - } - $insert_cols[] = 'user_id'; - $insert_values[] = $this->ID; - - $sql = "INSERT INTO ".$this->db->table_name('identities'). - " (changed, ".join(', ', $insert_cols).")". - " VALUES (".$this->db->now().", ".join(', ', array_pad(array(), sizeof($insert_values), '?')).")"; - - call_user_func_array(array($this->db, 'query'), - array_merge(array($sql), $insert_values)); - - return $this->db->insert_id('identities'); - } - - - /** - * Mark the given identity as deleted - * - * @param int $iid Identity ID - * @return boolean True if deleted successfully, false if nothing changed - */ - function delete_identity($iid) - { - if (!$this->ID) - return false; - - $sql_result = $this->db->query( - "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities'). - " WHERE user_id = ? AND del <> 1", - $this->ID); - - $sql_arr = $this->db->fetch_assoc($sql_result); - - // we'll not delete last identity - if ($sql_arr['ident_count'] <= 1) - return -1; - - $this->db->query( - "UPDATE ".$this->db->table_name('identities'). - " SET del = 1, changed = ".$this->db->now(). - " WHERE user_id = ?". - " AND identity_id = ?", - $this->ID, - $iid); - - return $this->db->affected_rows(); - } - - - /** - * Make this identity the default one for this user - * - * @param int $iid The identity ID - */ - function set_default($iid) - { - if ($this->ID && $iid) { - $this->db->query( - "UPDATE ".$this->db->table_name('identities'). - " SET ".$this->db->quoteIdentifier('standard')." = '0'". - " WHERE user_id = ?". - " AND identity_id <> ?". - " AND del <> 1", - $this->ID, - $iid); - } - } - - - /** - * Update user's last_login timestamp - */ - function touch() - { - if ($this->ID) { - $this->db->query( - "UPDATE ".$this->db->table_name('users'). - " SET last_login = ".$this->db->now(). - " WHERE user_id = ?", - $this->ID); - } - } - - - /** - * Clear the saved object state - */ - function reset() - { - $this->ID = null; - $this->data = null; - } - - - /** - * Find a user record matching the given name and host - * - * @param string $user IMAP user name - * @param string $host IMAP host name - * @return rcube_user New user instance - */ - static function query($user, $host) - { - $dbh = rcube::get_instance()->get_dbh(); - $config = rcube::get_instance()->config; - - // query for matching user name - $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users') - ." WHERE mail_host = ? AND username = ?", $host, $user); - - $sql_arr = $dbh->fetch_assoc($sql_result); - - // username not found, try aliases from identities - if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) { - $sql_result = $dbh->limitquery("SELECT u.*" - ." FROM " . $dbh->table_name('users') . " u" - ." JOIN " . $dbh->table_name('identities') . " i ON (i.user_id = u.user_id)" - ." WHERE email = ? AND del <> 1", 0, 1, $user); - - $sql_arr = $dbh->fetch_assoc($sql_result); - } - - // user already registered -> overwrite username - if ($sql_arr) - return new rcube_user($sql_arr['user_id'], $sql_arr); - else - return false; - } - - - /** - * Create a new user record and return a rcube_user instance - * - * @param string $user IMAP user name - * @param string $host IMAP host - * @return rcube_user New user instance - */ - static function create($user, $host) - { - $user_name = ''; - $user_email = ''; - $rcube = rcube::get_instance(); - $dbh = $rcube->get_dbh(); - - // try to resolve user in virtuser table and file - if ($email_list = self::user2email($user, false, true)) { - $user_email = is_array($email_list[0]) ? $email_list[0]['email'] : $email_list[0]; - } - - $data = $rcube->plugins->exec_hook('user_create', array( - 'host' => $host, - 'user' => $user, - 'user_name' => $user_name, - 'user_email' => $user_email, - 'email_list' => $email_list, - 'language' => $_SESSION['language'], - )); - - // plugin aborted this operation - if ($data['abort']) { - return false; - } - - $dbh->query( - "INSERT INTO ".$dbh->table_name('users'). - " (created, last_login, username, mail_host, language)". - " VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)", - strip_newlines($data['user']), - strip_newlines($data['host']), - strip_newlines($data['language'])); - - if ($user_id = $dbh->insert_id('users')) { - // create rcube_user instance to make plugin hooks work - $user_instance = new rcube_user($user_id, array( - 'user_id' => $user_id, - 'username' => $data['user'], - 'mail_host' => $data['host'], - 'language' => $data['language'], - )); - $rcube->user = $user_instance; - $mail_domain = $rcube->config->mail_domain($data['host']); - $user_name = $data['user_name']; - $user_email = $data['user_email']; - $email_list = $data['email_list']; - - if (empty($email_list)) { - if (empty($user_email)) { - $user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain); - } - $email_list[] = strip_newlines($user_email); - } - // identities_level check - else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) { - $email_list = array($email_list[0]); - } - - if (empty($user_name)) { - $user_name = $data['user']; - } - - // create new identities records - $standard = 1; - foreach ($email_list as $row) { - $record = array(); - - if (is_array($row)) { - if (empty($row['email'])) { - continue; - } - $record = $row; - } - else { - $record['email'] = $row; - } - - if (empty($record['name'])) { - $record['name'] = $user_name != $record['email'] ? $user_name : ''; - } - - $record['name'] = strip_newlines($record['name']); - $record['user_id'] = $user_id; - $record['standard'] = $standard; - - $plugin = $rcube->plugins->exec_hook('identity_create', - array('login' => true, 'record' => $record)); - - if (!$plugin['abort'] && $plugin['record']['email']) { - $rcube->user->insert_identity($plugin['record']); - } - $standard = 0; - } - } - else { - rcube::raise_error(array( - 'code' => 500, - 'type' => 'php', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Failed to create new user"), true, false); - } - - return $user_id ? $user_instance : false; - } - - - /** - * Resolve username using a virtuser plugins - * - * @param string $email E-mail address to resolve - * @return string Resolved IMAP username - */ - static function email2user($email) - { - $rcube = rcube::get_instance(); - $plugin = $rcube->plugins->exec_hook('email2user', - array('email' => $email, 'user' => NULL)); - - return $plugin['user']; - } - - - /** - * Resolve e-mail address from virtuser plugins - * - * @param string $user User name - * @param boolean $first If true returns first found entry - * @param boolean $extended If true returns email as array (email and name for identity) - * @return mixed Resolved e-mail address string or array of strings - */ - static function user2email($user, $first=true, $extended=false) - { - $rcube = rcube::get_instance(); - $plugin = $rcube->plugins->exec_hook('user2email', - array('email' => NULL, 'user' => $user, - 'first' => $first, 'extended' => $extended)); - - return empty($plugin['email']) ? NULL : $plugin['email']; - } - - - /** - * Return a list of saved searches linked with this user - * - * @param int $type Search type - * - * @return array List of saved searches indexed by search ID - */ - function list_searches($type) - { - $plugin = $this->rc->plugins->exec_hook('saved_search_list', array('type' => $type)); - - if ($plugin['abort']) { - return (array) $plugin['result']; - } - - $result = array(); - - $sql_result = $this->db->query( - "SELECT search_id AS id, ".$this->db->quoteIdentifier('name') - ." FROM ".$this->db->table_name('searches') - ." WHERE user_id = ?" - ." AND ".$this->db->quoteIdentifier('type')." = ?" - ." ORDER BY ".$this->db->quoteIdentifier('name'), - (int) $this->ID, (int) $type); - - while ($sql_arr = $this->db->fetch_assoc($sql_result)) { - $sql_arr['data'] = unserialize($sql_arr['data']); - $result[$sql_arr['id']] = $sql_arr; - } - - return $result; - } - - - /** - * Return saved search data. - * - * @param int $id Row identifier - * - * @return array Data - */ - function get_search($id) - { - $plugin = $this->rc->plugins->exec_hook('saved_search_get', array('id' => $id)); - - if ($plugin['abort']) { - return $plugin['result']; - } - - $sql_result = $this->db->query( - "SELECT ".$this->db->quoteIdentifier('name') - .", ".$this->db->quoteIdentifier('data') - .", ".$this->db->quoteIdentifier('type') - ." FROM ".$this->db->table_name('searches') - ." WHERE user_id = ?" - ." AND search_id = ?", - (int) $this->ID, (int) $id); - - while ($sql_arr = $this->db->fetch_assoc($sql_result)) { - return array( - 'id' => $id, - 'name' => $sql_arr['name'], - 'type' => $sql_arr['type'], - 'data' => unserialize($sql_arr['data']), - ); - } - - return null; - } - - - /** - * Deletes given saved search record - * - * @param int $sid Search ID - * - * @return boolean True if deleted successfully, false if nothing changed - */ - function delete_search($sid) - { - if (!$this->ID) - return false; - - $this->db->query( - "DELETE FROM ".$this->db->table_name('searches') - ." WHERE user_id = ?" - ." AND search_id = ?", - (int) $this->ID, $sid); - - return $this->db->affected_rows(); - } - - - /** - * Create a new saved search record linked with this user - * - * @param array $data Hash array with col->value pairs to save - * - * @return int The inserted search ID or false on error - */ - function insert_search($data) - { - if (!$this->ID) - return false; - - $insert_cols[] = 'user_id'; - $insert_values[] = (int) $this->ID; - $insert_cols[] = $this->db->quoteIdentifier('type'); - $insert_values[] = (int) $data['type']; - $insert_cols[] = $this->db->quoteIdentifier('name'); - $insert_values[] = $data['name']; - $insert_cols[] = $this->db->quoteIdentifier('data'); - $insert_values[] = serialize($data['data']); - - $sql = "INSERT INTO ".$this->db->table_name('searches') - ." (".join(', ', $insert_cols).")" - ." VALUES (".join(', ', array_pad(array(), sizeof($insert_values), '?')).")"; - - call_user_func_array(array($this->db, 'query'), - array_merge(array($sql), $insert_values)); - - return $this->db->insert_id('searches'); - } - -} diff --git a/program/include/rcube_utils.php b/program/include/rcube_utils.php deleted file mode 100644 index 2a4d4c482..000000000 --- a/program/include/rcube_utils.php +++ /dev/null @@ -1,885 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_utils.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2012, The Roundcube Dev Team | - | Copyright (C) 2011-2012, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Utility class providing common functions | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - | Author: Aleksander Machniak <alec@alec.pl> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Utility class providing common functions - * - * @package Core - */ -class rcube_utils -{ - // define constants for input reading - const INPUT_GET = 0x0101; - const INPUT_POST = 0x0102; - const INPUT_GPC = 0x0103; - - /** - * Helper method to set a cookie with the current path and host settings - * - * @param string Cookie name - * @param string Cookie value - * @param string Expiration time - */ - public static function setcookie($name, $value, $exp = 0) - { - if (headers_sent()) { - return; - } - - $cookie = session_get_cookie_params(); - $secure = $cookie['secure'] || self::https_check(); - - setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'], $secure, true); - } - - /** - * E-mail address validation. - * - * @param string $email Email address - * @param boolean $dns_check True to check dns - * - * @return boolean True on success, False if address is invalid - */ - public static function check_email($email, $dns_check=true) - { - // Check for invalid characters - if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email)) { - return false; - } - - // Check for length limit specified by RFC 5321 (#1486453) - if (strlen($email) > 254) { - return false; - } - - $email_array = explode('@', $email); - - // Check that there's one @ symbol - if (count($email_array) < 2) { - return false; - } - - $domain_part = array_pop($email_array); - $local_part = implode('@', $email_array); - - // from PEAR::Validate - $regexp = '&^(?: - ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name - ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) #2 OR dot-atom (RFC5322) - $&xi'; - - if (!preg_match($regexp, $local_part)) { - return false; - } - - // Validate domain part - if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) { - return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address - } - else { - // If not an IP address - $domain_array = explode('.', $domain_part); - // Not enough parts to be a valid domain - if (sizeof($domain_array) < 2) { - return false; - } - - foreach ($domain_array as $part) { - if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) { - return false; - } - } - - // last domain part - if (preg_match('/[^a-zA-Z]/', array_pop($domain_array))) { - return false; - } - - $rcube = rcube::get_instance(); - - if (!$dns_check || !$rcube->config->get('email_dns_check')) { - return true; - } - - if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) { - $lookup = array(); - @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup); - foreach ($lookup as $line) { - if (strpos($line, 'MX preference')) { - return true; - } - } - return false; - } - - // find MX record(s) - if (getmxrr($domain_part, $mx_records)) { - return true; - } - - // find any DNS record - if (checkdnsrr($domain_part, 'ANY')) { - return true; - } - } - - return false; - } - - - /** - * Validates IPv4 or IPv6 address - * - * @param string $ip IP address in v4 or v6 format - * - * @return bool True if the address is valid - */ - public static function check_ip($ip) - { - // IPv6, but there's no build-in IPv6 support - if (strpos($ip, ':') !== false && !defined('AF_INET6')) { - $parts = explode(':', $domain_part); - $count = count($parts); - - if ($count > 8 || $count < 2) { - return false; - } - - foreach ($parts as $idx => $part) { - $length = strlen($part); - if (!$length) { - // there can be only one :: - if ($found_empty) { - return false; - } - $found_empty = true; - } - // last part can be an IPv4 address - else if ($idx == $count - 1) { - if (!preg_match('/^[0-9a-f]{1,4}$/i', $part)) { - return @inet_pton($part) !== false; - } - } - else if (!preg_match('/^[0-9a-f]{1,4}$/i', $part)) { - return false; - } - } - - return true; - } - - return @inet_pton($ip) !== false; - } - - - /** - * Check whether the HTTP referer matches the current request - * - * @return boolean True if referer is the same host+path, false if not - */ - public static function check_referer() - { - $uri = parse_url($_SERVER['REQUEST_URI']); - $referer = parse_url(self::request_header('Referer')); - return $referer['host'] == self::request_header('Host') && $referer['path'] == $uri['path']; - } - - - /** - * Replacing specials characters to a specific encoding type - * - * @param string Input string - * @param string Encoding type: text|html|xml|js|url - * @param string Replace mode for tags: show|replace|remove - * @param boolean Convert newlines - * - * @return string The quoted string - */ - public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true) - { - static $html_encode_arr = false; - static $js_rep_table = false; - static $xml_rep_table = false; - - if (!is_string($str)) { - $str = strval($str); - } - - // encode for HTML output - if ($enctype == 'html') { - if (!$html_encode_arr) { - $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS); - unset($html_encode_arr['?']); - } - - $encode_arr = $html_encode_arr; - - // don't replace quotes and html tags - if ($mode == 'show' || $mode == '') { - $ltpos = strpos($str, '<'); - if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) { - unset($encode_arr['"']); - unset($encode_arr['<']); - unset($encode_arr['>']); - unset($encode_arr['&']); - } - } - else if ($mode == 'remove') { - $str = strip_tags($str); - } - - $out = strtr($str, $encode_arr); - - return $newlines ? nl2br($out) : $out; - } - - // if the replace tables for XML and JS are not yet defined - if ($js_rep_table === false) { - $js_rep_table = $xml_rep_table = array(); - $xml_rep_table['&'] = '&'; - - // can be increased to support more charsets - for ($c=160; $c<256; $c++) { - $xml_rep_table[chr($c)] = "&#$c;"; - } - - $xml_rep_table['"'] = '"'; - $js_rep_table['"'] = '\\"'; - $js_rep_table["'"] = "\\'"; - $js_rep_table["\\"] = "\\\\"; - // Unicode line and paragraph separators (#1486310) - $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '
'; - $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '
'; - } - - // encode for javascript use - if ($enctype == 'js') { - return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table)); - } - - // encode for plaintext - if ($enctype == 'text') { - return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str); - } - - if ($enctype == 'url') { - return rawurlencode($str); - } - - // encode for XML - if ($enctype == 'xml') { - return strtr($str, $xml_rep_table); - } - - // no encoding given -> return original string - return $str; - } - - - /** - * Read input value and convert it for internal use - * Performs stripslashes() and charset conversion if necessary - * - * @param string Field name to read - * @param int Source to get value from (GPC) - * @param boolean Allow HTML tags in field value - * @param string Charset to convert into - * - * @return string Field value or NULL if not available - */ - public static function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL) - { - $value = NULL; - - if ($source == self::INPUT_GET) { - if (isset($_GET[$fname])) { - $value = $_GET[$fname]; - } - } - else if ($source == self::INPUT_POST) { - if (isset($_POST[$fname])) { - $value = $_POST[$fname]; - } - } - else if ($source == self::INPUT_GPC) { - if (isset($_POST[$fname])) { - $value = $_POST[$fname]; - } - else if (isset($_GET[$fname])) { - $value = $_GET[$fname]; - } - else if (isset($_COOKIE[$fname])) { - $value = $_COOKIE[$fname]; - } - } - - return self::parse_input_value($value, $allow_html, $charset); - } - - - /** - * Parse/validate input value. See self::get_input_value() - * Performs stripslashes() and charset conversion if necessary - * - * @param string Input value - * @param boolean Allow HTML tags in field value - * @param string Charset to convert into - * - * @return string Parsed value - */ - public static function parse_input_value($value, $allow_html=FALSE, $charset=NULL) - { - global $OUTPUT; - - if (empty($value)) { - return $value; - } - - if (is_array($value)) { - foreach ($value as $idx => $val) { - $value[$idx] = self::parse_input_value($val, $allow_html, $charset); - } - return $value; - } - - // strip single quotes if magic_quotes_sybase is enabled - if (ini_get('magic_quotes_sybase')) { - $value = str_replace("''", "'", $value); - } - // strip slashes if magic_quotes enabled - else if (get_magic_quotes_gpc() || get_magic_quotes_runtime()) { - $value = stripslashes($value); - } - - // remove HTML tags if not allowed - if (!$allow_html) { - $value = strip_tags($value); - } - - $output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null; - - // remove invalid characters (#1488124) - if ($output_charset == 'UTF-8') { - $value = rcube_charset::clean($value); - } - - // convert to internal charset - if ($charset && $output_charset) { - $value = rcube_charset::convert($value, $output_charset, $charset); - } - - return $value; - } - - - /** - * Convert array of request parameters (prefixed with _) - * to a regular array with non-prefixed keys. - * - * @param int $mode Source to get value from (GPC) - * @param string $ignore PCRE expression to skip parameters by name - * - * @return array Hash array with all request parameters - */ - public static function request2param($mode = null, $ignore = 'task|action') - { - $out = array(); - $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST); - - foreach ($src as $key => $value) { - $fname = $key[0] == '_' ? substr($key, 1) : $key; - if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) { - $out[$fname] = self::get_input_value($key, $mode); - } - } - - return $out; - } - - - /** - * Convert the given string into a valid HTML identifier - * Same functionality as done in app.js with rcube_webmail.html_identifier() - */ - public static function html_identifier($str, $encode=false) - { - if ($encode) { - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); - } - else { - return asciiwords($str, true, '_'); - } - } - - - /** - * Create an edit field for inclusion on a form - * - * @param string col field name - * @param string value field value - * @param array attrib HTML element attributes for field - * @param string type HTML element type (default 'text') - * - * @return string HTML field definition - */ - public static function get_edit_field($col, $value, $attrib, $type = 'text') - { - static $colcounts = array(); - - $fname = '_'.$col; - $attrib['name'] = $fname . ($attrib['array'] ? '[]' : ''); - $attrib['class'] = trim($attrib['class'] . ' ff_' . $col); - - if ($type == 'checkbox') { - $attrib['value'] = '1'; - $input = new html_checkbox($attrib); - } - else if ($type == 'textarea') { - $attrib['cols'] = $attrib['size']; - $input = new html_textarea($attrib); - } - else if ($type == 'select') { - $input = new html_select($attrib); - $input->add('---', ''); - $input->add(array_values($attrib['options']), array_keys($attrib['options'])); - } - else if ($attrib['type'] == 'password') { - $input = new html_passwordfield($attrib); - } - else { - if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden') { - $attrib['type'] = 'text'; - } - $input = new html_inputfield($attrib); - } - - // use value from post - if (isset($_POST[$fname])) { - $postvalue = self::get_input_value($fname, self::INPUT_POST, true); - $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue; - } - - $out = $input->show($value); - - return $out; - } - - - /** - * Replace all css definitions with #container [def] - * and remove css-inlined scripting - * - * @param string CSS source code - * @param string Container ID to use as prefix - * - * @return string Modified CSS source - */ - public static function mod_css_styles($source, $container_id, $allow_remote=false) - { - $last_pos = 0; - $replacements = new rcube_string_replacer; - - // ignore the whole block if evil styles are detected - $source = self::xss_entity_decode($source); - $stripped = preg_replace('/[^a-z\(:;]/i', '', $source); - $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : ''); - if (preg_match("/$evilexpr/i", $stripped)) { - return '/* evil! */'; - } - - // cut out all contents between { and } - while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) { - $styles = substr($source, $pos+1, $pos2-($pos+1)); - - // check every line of a style block... - if ($allow_remote) { - $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY); - foreach ($a_styles as $line) { - $stripped = preg_replace('/[^a-z\(:;]/i', '', $line); - // ... and only allow strict url() values - $regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims'; - if (stripos($stripped, 'url(') && !preg_match($regexp, $line)) { - $a_styles = array('/* evil! */'); - break; - } - } - $styles = join(";\n", $a_styles); - } - - $key = $replacements->add($styles); - $source = substr($source, 0, $pos+1) - . $replacements->get_replacement($key) - . substr($source, $pos2, strlen($source)-$pos2); - $last_pos = $pos+2; - } - - // remove html comments and add #container to each tag selector. - // also replace body definition because we also stripped off the <body> tag - $styles = preg_replace( - array( - '/(^\s*<!--)|(-->\s*$)/', - '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im', - '/'.preg_quote($container_id, '/').'\s+body/i', - ), - array( - '', - "\\1#$container_id \\2", - $container_id, - ), - $source); - - // put block contents back in - $styles = $replacements->resolve($styles); - - return $styles; - } - - - /** - * Generate CSS classes from mimetype and filename extension - * - * @param string $mimetype Mimetype - * @param string $filename Filename - * - * @return string CSS classes separated by space - */ - public static function file2class($mimetype, $filename) - { - list($primary, $secondary) = explode('/', $mimetype); - - $classes = array($primary ? $primary : 'unknown'); - if ($secondary) { - $classes[] = $secondary; - } - if (preg_match('/\.([a-z0-9]+)$/i', $filename, $m)) { - $classes[] = $m[1]; - } - - return strtolower(join(" ", $classes)); - } - - - /** - * Decode escaped entities used by known XSS exploits. - * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples - * - * @param string CSS content to decode - * - * @return string Decoded string - */ - public static function xss_entity_decode($content) - { - $out = html_entity_decode(html_entity_decode($content)); - $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', - array(self, 'xss_entity_decode_callback'), $out); - $out = preg_replace('#/\*.*\*/#Ums', '', $out); - - return $out; - } - - - /** - * preg_replace_callback callback for xss_entity_decode - * - * @param array $matches Result from preg_replace_callback - * - * @return string Decoded entity - */ - public static function xss_entity_decode_callback($matches) - { - return chr(hexdec($matches[1])); - } - - - /** - * Check if we can process not exceeding memory_limit - * - * @param integer Required amount of memory - * - * @return boolean True if memory won't be exceeded, False otherwise - */ - public static function mem_check($need) - { - $mem_limit = parse_bytes(ini_get('memory_limit')); - $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB - - return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true; - } - - - /** - * Check if working in SSL mode - * - * @param integer $port HTTPS port number - * @param boolean $use_https Enables 'use_https' option checking - * - * @return boolean - */ - public static function https_check($port=null, $use_https=true) - { - global $RCMAIL; - - if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') { - return true; - } - if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') { - return true; - } - if ($port && $_SERVER['SERVER_PORT'] == $port) { - return true; - } - if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https')) { - return true; - } - - return false; - } - - - /** - * Replaces hostname variables. - * - * @param string $name Hostname - * @param string $host Optional IMAP hostname - * - * @return string Hostname - */ - public static function parse_host($name, $host = '') - { - // %n - host - $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']); - // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld - $t = preg_replace('/^[^\.]+\./', '', $n); - // %d - domain name without first part - $d = preg_replace('/^[^\.]+\./', '', $_SERVER['HTTP_HOST']); - // %h - IMAP host - $h = $_SESSION['storage_host'] ? $_SESSION['storage_host'] : $host; - // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld - $z = preg_replace('/^[^\.]+\./', '', $h); - // %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided - if (strpos($name, '%s') !== false) { - $user_email = self::get_input_value('_user', self::INPUT_POST); - $user_email = self::idn_convert($user_email, true); - $matches = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s); - if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) { - return false; - } - } - - $name = str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name); - return $name; - } - - - /** - * Returns remote IP address and forwarded addresses if found - * - * @return string Remote IP address(es) - */ - public static function remote_ip() - { - $address = $_SERVER['REMOTE_ADDR']; - - // append the NGINX X-Real-IP header, if set - if (!empty($_SERVER['HTTP_X_REAL_IP'])) { - $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP']; - } - // append the X-Forwarded-For header, if set - if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR']; - } - - if (!empty($remote_ip)) { - $address .= '(' . implode(',', $remote_ip) . ')'; - } - - return $address; - } - - - /** - * Read a specific HTTP request header. - * - * @param string $name Header name - * - * @return mixed Header value or null if not available - */ - public static function request_header($name) - { - if (function_exists('getallheaders')) { - $hdrs = array_change_key_case(getallheaders(), CASE_UPPER); - $key = strtoupper($name); - } - else { - $key = 'HTTP_' . strtoupper(strtr($name, '-', '_')); - $hdrs = array_change_key_case($_SERVER, CASE_UPPER); - } - - return $hdrs[$key]; - } - - /** - * Explode quoted string - * - * @param string Delimiter expression string for preg_match() - * @param string Input string - * - * @return array String items - */ - public static function explode_quoted_string($delimiter, $string) - { - $result = array(); - $strlen = strlen($string); - - for ($q=$p=$i=0; $i < $strlen; $i++) { - if ($string[$i] == "\"" && $string[$i-1] != "\\") { - $q = $q ? false : true; - } - else if (!$q && preg_match("/$delimiter/", $string[$i])) { - $result[] = substr($string, $p, $i - $p); - $p = $i + 1; - } - } - - $result[] = substr($string, $p); - - return $result; - } - - - /** - * Improved equivalent to strtotime() - * - * @param string $date Date string - * - * @return int Unix timestamp - */ - public static function strtotime($date) - { - // check for MS Outlook vCard date format YYYYMMDD - if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) { - return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1])); - } - else if (is_numeric($date)) { - return $date; - } - - // support non-standard "GMTXXXX" literal - $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date); - - // if date parsing fails, we have a date in non-rfc format. - // remove token from the end and try again - while ((($ts = @strtotime($date)) === false) || ($ts < 0)) { - $d = explode(' ', $date); - array_pop($d); - if (!$d) { - break; - } - $date = implode(' ', $d); - } - - return $ts; - } - - - /* - * Idn_to_ascii wrapper. - * Intl/Idn modules version of this function doesn't work with e-mail address - */ - public static function idn_to_ascii($str) - { - return self::idn_convert($str, true); - } - - - /* - * Idn_to_ascii wrapper. - * Intl/Idn modules version of this function doesn't work with e-mail address - */ - public static function idn_to_utf8($str) - { - return self::idn_convert($str, false); - } - - - public static function idn_convert($input, $is_utf=false) - { - if ($at = strpos($input, '@')) { - $user = substr($input, 0, $at); - $domain = substr($input, $at+1); - } - else { - $domain = $input; - } - - $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain); - - if ($domain === false) { - return ''; - } - - return $at ? $user . '@' . $domain : $domain; - } - - /** - * Split the given string into word tokens - * - * @param string Input to tokenize - * @return array List of tokens - */ - public static function tokenize_string($str) - { - return explode(" ", preg_replace( - array('/[\s;\/+-]+/i', '/(\d)[-.\s]+(\d)/', '/\s\w{1,3}\s/u'), - array(' ', '\\1\\2', ' '), - $str)); - } - - /** - * Normalize the given string for fulltext search. - * Currently only optimized for Latin-1 characters; to be extended - * - * @param string Input string (UTF-8) - * @param boolean True to return list of words as array - * @return mixed Normalized string or a list of normalized tokens - */ - public static function normalize_string($str, $as_array = false) - { - // split by words - $arr = self::tokenize_string($str); - - foreach ($arr as $i => $part) { - if (utf8_encode(utf8_decode($part)) == $part) { // is latin-1 ? - $arr[$i] = utf8_encode(strtr(strtolower(strtr(utf8_decode($part), - 'ÇçäâàåéêëèïîìÅÉöôòüûùÿøØáíóúñÑÁÂÀãÃÊËÈÍÎÏÓÔõÕÚÛÙýÝ', - 'ccaaaaeeeeiiiaeooouuuyooaiounnaaaaaeeeiiioooouuuyy')), - array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'))); - } - else - $arr[$i] = mb_strtolower($part); - } - - return $as_array ? $arr : join(" ", $arr); - } - -} diff --git a/program/include/rcube_vcard.php b/program/include/rcube_vcard.php deleted file mode 100644 index 00903c257..000000000 --- a/program/include/rcube_vcard.php +++ /dev/null @@ -1,825 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | program/include/rcube_vcard.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2008-2011, The Roundcube Dev Team | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Logical representation of a vcard address record | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Logical representation of a vcard-based address record - * Provides functions to parse and export vCard data format - * - * @package Addressbook - * @author Thomas Bruederli <roundcube@gmail.com> - */ -class rcube_vcard -{ - private static $values_decoded = false; - private $raw = array( - 'FN' => array(), - 'N' => array(array('','','','','')), - ); - private static $fieldmap = array( - 'phone' => 'TEL', - 'birthday' => 'BDAY', - 'website' => 'URL', - 'notes' => 'NOTE', - 'email' => 'EMAIL', - 'address' => 'ADR', - 'jobtitle' => 'TITLE', - 'department' => 'X-DEPARTMENT', - 'gender' => 'X-GENDER', - 'maidenname' => 'X-MAIDENNAME', - 'anniversary' => 'X-ANNIVERSARY', - 'assistant' => 'X-ASSISTANT', - 'manager' => 'X-MANAGER', - 'spouse' => 'X-SPOUSE', - 'edit' => 'X-AB-EDIT', - ); - private $typemap = array('IPHONE' => 'mobile', 'CELL' => 'mobile', 'WORK,FAX' => 'workfax'); - private $phonetypemap = array('HOME1' => 'HOME', 'BUSINESS1' => 'WORK', 'BUSINESS2' => 'WORK2', 'BUSINESSFAX' => 'WORK,FAX'); - private $addresstypemap = array('BUSINESS' => 'WORK'); - private $immap = array('X-JABBER' => 'jabber', 'X-ICQ' => 'icq', 'X-MSN' => 'msn', 'X-AIM' => 'aim', 'X-YAHOO' => 'yahoo', 'X-SKYPE' => 'skype', 'X-SKYPE-USERNAME' => 'skype'); - - public $business = false; - public $displayname; - public $surname; - public $firstname; - public $middlename; - public $nickname; - public $organization; - public $notes; - public $email = array(); - - public static $eol = "\r\n"; - - /** - * Constructor - */ - public function __construct($vcard = null, $charset = RCMAIL_CHARSET, $detect = false, $fieldmap = array()) - { - if (!empty($fielmap)) - $this->extend_fieldmap($fieldmap); - - if (!empty($vcard)) - $this->load($vcard, $charset, $detect); - } - - - /** - * Load record from (internal, unfolded) vcard 3.0 format - * - * @param string vCard string to parse - * @param string Charset of string values - * @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required - */ - public function load($vcard, $charset = RCMAIL_CHARSET, $detect = false) - { - self::$values_decoded = false; - $this->raw = self::vcard_decode($vcard); - - // resolve charset parameters - if ($charset == null) { - $this->raw = self::charset_convert($this->raw); - } - // vcard has encoded values and charset should be detected - else if ($detect && self::$values_decoded && - ($detected_charset = self::detect_encoding(self::vcard_encode($this->raw))) && $detected_charset != RCMAIL_CHARSET) { - $this->raw = self::charset_convert($this->raw, $detected_charset); - } - - // consider FN empty if the same as the primary e-mail address - if ($this->raw['FN'][0][0] == $this->raw['EMAIL'][0][0]) - $this->raw['FN'][0][0] = ''; - - // find well-known address fields - $this->displayname = $this->raw['FN'][0][0]; - $this->surname = $this->raw['N'][0][0]; - $this->firstname = $this->raw['N'][0][1]; - $this->middlename = $this->raw['N'][0][2]; - $this->nickname = $this->raw['NICKNAME'][0][0]; - $this->organization = $this->raw['ORG'][0][0]; - $this->business = ($this->raw['X-ABSHOWAS'][0][0] == 'COMPANY') || (join('', (array)$this->raw['N'][0]) == '' && !empty($this->organization)); - - foreach ((array)$this->raw['EMAIL'] as $i => $raw_email) - $this->email[$i] = is_array($raw_email) ? $raw_email[0] : $raw_email; - - // make the pref e-mail address the first entry in $this->email - $pref_index = $this->get_type_index('EMAIL', 'pref'); - if ($pref_index > 0) { - $tmp = $this->email[0]; - $this->email[0] = $this->email[$pref_index]; - $this->email[$pref_index] = $tmp; - } - } - - - /** - * Return vCard data as associative array to be unsed in Roundcube address books - * - * @return array Hash array with key-value pairs - */ - public function get_assoc() - { - $out = array('name' => $this->displayname); - $typemap = $this->typemap; - - // copy name fields to output array - foreach (array('firstname','surname','middlename','nickname','organization') as $col) { - if (strlen($this->$col)) - $out[$col] = $this->$col; - } - - if ($this->raw['N'][0][3]) - $out['prefix'] = $this->raw['N'][0][3]; - if ($this->raw['N'][0][4]) - $out['suffix'] = $this->raw['N'][0][4]; - - // convert from raw vcard data into associative data for Roundcube - foreach (array_flip(self::$fieldmap) as $tag => $col) { - foreach ((array)$this->raw[$tag] as $i => $raw) { - if (is_array($raw)) { - $k = -1; - $key = $col; - $subtype = ''; - - if (!empty($raw['type'])) { - $combined = join(',', self::array_filter((array)$raw['type'], 'internet,pref', true)); - $combined = strtoupper($combined); - - if ($typemap[$combined]) { - $subtype = $typemap[$combined]; - } - else if ($typemap[$raw['type'][++$k]]) { - $subtype = $typemap[$raw['type'][$k]]; - } - else { - $subtype = strtolower($raw['type'][$k]); - } - - while ($k < count($raw['type']) && ($subtype == 'internet' || $subtype == 'pref')) - $subtype = $typemap[$raw['type'][++$k]] ? $typemap[$raw['type'][$k]] : strtolower($raw['type'][$k]); - } - - // read vcard 2.1 subtype - if (!$subtype) { - foreach ($raw as $k => $v) { - if (!is_numeric($k) && $v === true && ($k = strtolower($k)) - && !in_array($k, array('pref','internet','voice','base64')) - ) { - $k_uc = strtoupper($k); - $subtype = $typemap[$k_uc] ? $typemap[$k_uc] : $k; - break; - } - } - } - - // force subtype if none set - if (!$subtype && preg_match('/^(email|phone|address|website)/', $key)) - $subtype = 'other'; - - if ($subtype) - $key .= ':' . $subtype; - - // split ADR values into assoc array - if ($tag == 'ADR') { - list(,, $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']) = $raw; - $out[$key][] = $value; - } - else - $out[$key][] = $raw[0]; - } - else { - $out[$col][] = $raw; - } - } - } - - // handle special IM fields as used by Apple - foreach ($this->immap as $tag => $type) { - foreach ((array)$this->raw[$tag] as $i => $raw) { - $out['im:'.$type][] = $raw[0]; - } - } - - // copy photo data - if ($this->raw['PHOTO']) - $out['photo'] = $this->raw['PHOTO'][0][0]; - - return $out; - } - - - /** - * Convert the data structure into a vcard 3.0 string - */ - public function export($folded = true) - { - $vcard = self::vcard_encode($this->raw); - return $folded ? self::rfc2425_fold($vcard) : $vcard; - } - - - /** - * Clear the given fields in the loaded vcard data - * - * @param array List of field names to be reset - */ - public function reset($fields = null) - { - if (!$fields) - $fields = array_merge(array_values(self::$fieldmap), array_keys($this->immap), array('FN','N','ORG','NICKNAME','EMAIL','ADR','BDAY')); - - foreach ($fields as $f) - unset($this->raw[$f]); - - if (!$this->raw['N']) - $this->raw['N'] = array(array('','','','','')); - if (!$this->raw['FN']) - $this->raw['FN'] = array(); - - $this->email = array(); - } - - - /** - * Setter for address record fields - * - * @param string Field name - * @param string Field value - * @param string Type/section name - */ - public function set($field, $value, $type = 'HOME') - { - $field = strtolower($field); - $type_uc = strtoupper($type); - $typemap = array_flip($this->typemap); - - switch ($field) { - case 'name': - case 'displayname': - $this->raw['FN'][0][0] = $value; - break; - - case 'surname': - $this->raw['N'][0][0] = $value; - break; - - case 'firstname': - $this->raw['N'][0][1] = $value; - break; - - case 'middlename': - $this->raw['N'][0][2] = $value; - break; - - case 'prefix': - $this->raw['N'][0][3] = $value; - break; - - case 'suffix': - $this->raw['N'][0][4] = $value; - break; - - case 'nickname': - $this->raw['NICKNAME'][0][0] = $value; - break; - - case 'organization': - $this->raw['ORG'][0][0] = $value; - break; - - case 'photo': - if (strpos($value, 'http:') === 0) { - // TODO: fetch file from URL and save it locally? - $this->raw['PHOTO'][0] = array(0 => $value, 'url' => true); - } - else { - $this->raw['PHOTO'][0] = array(0 => $value, 'base64' => (bool) preg_match('![^a-z0-9/=+-]!i', $value)); - } - break; - - case 'email': - $this->raw['EMAIL'][] = array(0 => $value, 'type' => array_filter(array('INTERNET', $type_uc))); - $this->email[] = $value; - break; - - case 'im': - // save IM subtypes into extension fields - $typemap = array_flip($this->immap); - if ($field = $typemap[strtolower($type)]) - $this->raw[$field][] = array(0 => $value); - break; - - case 'birthday': - case 'anniversary': - if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) - $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date')); - break; - - case 'address': - if ($this->addresstypemap[$type_uc]) - $type = $this->addresstypemap[$type_uc]; - - $value = $value[0] ? $value : array('', '', $value['street'], $value['locality'], $value['region'], $value['zipcode'], $value['country']); - - // fall through if not empty - if (!strlen(join('', $value))) - break; - - default: - if ($field == 'phone' && $this->phonetypemap[$type_uc]) - $type = $this->phonetypemap[$type_uc]; - - if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) { - $index = count($this->raw[$tag]); - $this->raw[$tag][$index] = (array)$value; - if ($type) - $this->raw[$tag][$index]['type'] = explode(',', ($typemap[$type_uc] ? $typemap[$type_uc] : $type)); - } - break; - } - } - - /** - * Setter for individual vcard properties - * - * @param string VCard tag name - * @param array Value-set of this vcard property - * @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set - */ - public function set_raw($tag, $value, $append = false) - { - $index = $append ? count($this->raw[$tag]) : 0; - $this->raw[$tag][$index] = (array)$value; - } - - - /** - * Find index with the '$type' attribute - * - * @param string Field name - * @return int Field index having $type set - */ - private function get_type_index($field, $type = 'pref') - { - $result = 0; - if ($this->raw[$field]) { - foreach ($this->raw[$field] as $i => $data) { - if (is_array($data['type']) && in_array_nocase('pref', $data['type'])) - $result = $i; - } - } - - return $result; - } - - - /** - * Convert a whole vcard (array) to UTF-8. - * If $force_charset is null, each member value that has a charset parameter will be converted - */ - private static function charset_convert($card, $force_charset = null) - { - foreach ($card as $key => $node) { - foreach ($node as $i => $subnode) { - if (is_array($subnode) && (($charset = $force_charset) || ($subnode['charset'] && ($charset = $subnode['charset'][0])))) { - foreach ($subnode as $j => $value) { - if (is_numeric($j) && is_string($value)) - $card[$key][$i][$j] = rcube_charset::convert($value, $charset); - } - unset($card[$key][$i]['charset']); - } - } - } - - return $card; - } - - - /** - * Extends fieldmap definition - */ - public function extend_fieldmap($map) - { - if (is_array($map)) - self::$fieldmap = array_merge($map, self::$fieldmap); - } - - - /** - * Factory method to import a vcard file - * - * @param string vCard file content - * @return array List of rcube_vcard objects - */ - public static function import($data) - { - $out = array(); - - // check if charsets are specified (usually vcard version < 3.0 but this is not reliable) - if (preg_match('/charset=/i', substr($data, 0, 2048))) - $charset = null; - // detect charset and convert to utf-8 - else if (($charset = self::detect_encoding($data)) && $charset != RCMAIL_CHARSET) { - $data = rcube_charset::convert($data, $charset); - $data = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $data); // also remove BOM - $charset = RCMAIL_CHARSET; - } - - $vcard_block = ''; - $in_vcard_block = false; - - foreach (preg_split("/[\r\n]+/", $data) as $i => $line) { - if ($in_vcard_block && !empty($line)) - $vcard_block .= $line . "\n"; - - $line = trim($line); - - if (preg_match('/^END:VCARD$/i', $line)) { - // parse vcard - $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap); - if (!empty($obj->displayname) || !empty($obj->email)) - $out[] = $obj; - - $in_vcard_block = false; - } - else if (preg_match('/^BEGIN:VCARD$/i', $line)) { - $vcard_block = $line . "\n"; - $in_vcard_block = true; - } - } - - return $out; - } - - - /** - * Normalize vcard data for better parsing - * - * @param string vCard block - * @return string Cleaned vcard block - */ - private static function cleanup($vcard) - { - // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;) - $vcard = preg_replace( - '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', - '\2;type=\5\3:\4', - $vcard); - - // convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility - $vcard = preg_replace_callback( - '/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', - array('self', 'x_abrelatednames_callback'), - $vcard); - - // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines - $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard); - - // convert X-WAB-GENDER to X-GENDER - if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) { - $value = $matches[1] == '2' ? 'male' : 'female'; - $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard); - } - - // if N doesn't have any semicolons, add some - $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard); - - return $vcard; - } - - private static function x_abrelatednames_callback($matches) - { - return 'X-' . strtoupper($matches[5]) . $matches[3] . ':'. $matches[4]; - } - - private static function rfc2425_fold_callback($matches) - { - // chunk_split string and avoid lines breaking multibyte characters - $c = 71; - $out .= substr($matches[1], 0, $c); - for ($n = $c; $c < strlen($matches[1]); $c++) { - // break if length > 75 or mutlibyte character starts after position 71 - if ($n > 75 || ($n > 71 && ord($matches[1][$c]) >> 6 == 3)) { - $out .= "\r\n "; - $n = 0; - } - $out .= $matches[1][$c]; - $n++; - } - - return $out; - } - - public static function rfc2425_fold($val) - { - return preg_replace_callback('/([^\n]{72,})/', array('self', 'rfc2425_fold_callback'), $val); - } - - - /** - * Decodes a vcard block (vcard 3.0 format, unfolded) - * into an array structure - * - * @param string vCard block to parse - * @return array Raw data structure - */ - private static function vcard_decode($vcard) - { - // Perform RFC2425 line unfolding and split lines - $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard); - $lines = explode("\n", $vcard); - $data = array(); - - for ($i=0; $i < count($lines); $i++) { - if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line)) - continue; - - if (preg_match('/^(BEGIN|END)$/i', $line[1])) - continue; - - // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:" - if (($data['VERSION'][0] == "2.1") && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2) && !preg_match('/^TYPE=/i', $regs2[2])) { - $line[1] = $regs2[1]; - foreach (explode(';', $regs2[2]) as $prop) - $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop); - } - - if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) { - $entry = array(); - $field = strtoupper($regs2[1][0]); - $enc = null; - - foreach($regs2[1] as $attrid => $attr) { - if ((list($key, $value) = explode('=', $attr)) && $value) { - $value = trim($value); - if ($key == 'ENCODING') { - $value = strtoupper($value); - // add next line(s) to value string if QP line end detected - if ($value == 'QUOTED-PRINTABLE') { - while (preg_match('/=$/', $lines[$i])) - $line[2] .= "\n" . $lines[++$i]; - } - $enc = $value; - } - else { - $lc_key = strtolower($key); - $entry[$lc_key] = array_merge((array)$entry[$lc_key], (array)self::vcard_unquote($value, ',')); - } - } - else if ($attrid > 0) { - $entry[strtolower($key)] = true; // true means attr without =value - } - } - - // decode value - if ($enc || !empty($entry['base64'])) { - // save encoding type (#1488432) - if ($enc == 'B') { - $entry['encoding'] = 'B'; - // should we use vCard 3.0 instead? - // $entry['base64'] = true; - } - $line[2] = self::decode_value($line[2], $enc ? $enc : 'base64'); - } - - if ($enc != 'B' && empty($entry['base64'])) { - $line[2] = self::vcard_unquote($line[2]); - } - - $entry = array_merge($entry, (array) $line[2]); - $data[$field][] = $entry; - } - } - - unset($data['VERSION']); - return $data; - } - - - /** - * Decode a given string with the encoding rule from ENCODING attributes - * - * @param string String to decode - * @param string Encoding type (quoted-printable and base64 supported) - * @return string Decoded 8bit value - */ - private static function decode_value($value, $encoding) - { - switch (strtolower($encoding)) { - case 'quoted-printable': - self::$values_decoded = true; - return quoted_printable_decode($value); - - case 'base64': - case 'b': - self::$values_decoded = true; - return base64_decode($value); - - default: - return $value; - } - } - - - /** - * Encodes an entry for storage in our database (vcard 3.0 format, unfolded) - * - * @param array Raw data structure to encode - * @return string vCard encoded string - */ - static function vcard_encode($data) - { - foreach((array)$data as $type => $entries) { - /* valid N has 5 properties */ - while ($type == "N" && is_array($entries[0]) && count($entries[0]) < 5) - $entries[0][] = ""; - - // make sure FN is not empty (required by RFC2426) - if ($type == "FN" && empty($entries)) - $entries[0] = $data['EMAIL'][0][0]; - - foreach((array)$entries as $entry) { - $attr = ''; - if (is_array($entry)) { - $value = array(); - foreach($entry as $attrname => $attrvalues) { - if (is_int($attrname)) { - if (!empty($entry['base64']) || $entry['encoding'] == 'B') { - $attrvalues = base64_encode($attrvalues); - } - $value[] = $attrvalues; - } - else if (is_bool($attrvalues)) { - if ($attrvalues) { - $attr .= strtoupper(";$attrname"); // true means just tag, not tag=value, as in PHOTO;BASE64:... - } - } - else { - foreach((array)$attrvalues as $attrvalue) - $attr .= strtoupper(";$attrname=") . self::vcard_quote($attrvalue, ','); - } - } - } - else { - $value = $entry; - } - - // skip empty entries - if (self::is_empty($value)) - continue; - - $vcard .= self::vcard_quote($type) . $attr . ':' . self::vcard_quote($value) . self::$eol; - } - } - - return 'BEGIN:VCARD' . self::$eol . 'VERSION:3.0' . self::$eol . $vcard . 'END:VCARD'; - } - - - /** - * Join indexed data array to a vcard quoted string - * - * @param array Field data - * @param string Separator - * @return string Joined and quoted string - */ - private static function vcard_quote($s, $sep = ';') - { - if (is_array($s)) { - foreach($s as $part) { - $r[] = self::vcard_quote($part, $sep); - } - return(implode($sep, (array)$r)); - } - else { - return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;')); - } - } - - - /** - * Split quoted string - * - * @param string vCard string to split - * @param string Separator char/string - * @return array List with splited values - */ - private static function vcard_unquote($s, $sep = ';') - { - // break string into parts separated by $sep, but leave escaped $sep alone - if (count($parts = explode($sep, strtr($s, array("\\$sep" => "\007")))) > 1) { - foreach($parts as $s) { - $result[] = self::vcard_unquote(strtr($s, array("\007" => "\\$sep")), $sep); - } - return $result; - } - else { - return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';', '\:' => ':')); - } - } - - - /** - * Check if vCard entry is empty: empty string or an array with - * all entries empty. - * - * @param mixed $value Attribute value (string or array) - * - * @return bool True if the value is empty, False otherwise - */ - private static function is_empty($value) - { - foreach ((array)$value as $v) { - if (((string)$v) !== '') { - return false; - } - } - - return true; - } - - /** - * Extract array values by a filter - * - * @param array Array to filter - * @param keys Array or comma separated list of values to keep - * @param boolean Invert key selection: remove the listed values - * @return array The filtered array - */ - private static function array_filter($arr, $values, $inverse = false) - { - if (!is_array($values)) - $values = explode(',', $values); - - $result = array(); - $keep = array_flip((array)$values); - foreach ($arr as $key => $val) - if ($inverse != isset($keep[strtolower($val)])) - $result[$key] = $val; - - return $result; - } - - /** - * Returns UNICODE type based on BOM (Byte Order Mark) - * - * @param string Input string to test - * @return string Detected encoding - */ - private static function detect_encoding($string) - { - if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian - if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian - if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE'; // Big Endian - if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE'; // Little Endian - if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8'; - - // heuristics - if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE'; - if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE'; - if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE'; - if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE'; - - // use mb_detect_encoding() - $encodings = array('UTF-8', 'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', - 'ISO-8859-4', 'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9', - 'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16', - 'WINDOWS-1252', 'WINDOWS-1251', 'BIG5', 'GB2312'); - - if (function_exists('mb_detect_encoding') && ($enc = mb_detect_encoding($string, $encodings))) - return $enc; - - // No match, check for UTF-8 - // from http://w3.org/International/questions/qa-forms-utf-8.html - if (preg_match('/\A( - [\x09\x0A\x0D\x20-\x7E] - | [\xC2-\xDF][\x80-\xBF] - | \xE0[\xA0-\xBF][\x80-\xBF] - | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} - | \xED[\x80-\x9F][\x80-\xBF] - | \xF0[\x90-\xBF][\x80-\xBF]{2} - | [\xF1-\xF3][\x80-\xBF]{3} - | \xF4[\x80-\x8F][\x80-\xBF]{2} - )*\z/xs', substr($string, 0, 2048))) - return 'UTF-8'; - - return rcube::get_instance()->config->get('default_charset', 'ISO-8859-1'); # fallback to Latin-1 - } - -} |