diff options
Diffstat (limited to 'program/lib/Roundcube')
30 files changed, 1498 insertions, 1403 deletions
diff --git a/program/lib/Roundcube/bootstrap.php b/program/lib/Roundcube/bootstrap.php index 65ef98ebd..98bbce5d4 100644 --- a/program/lib/Roundcube/bootstrap.php +++ b/program/lib/Roundcube/bootstrap.php @@ -58,7 +58,7 @@ define('RCUBE_VERSION', '1.1-git'); define('RCUBE_CHARSET', 'UTF-8'); if (!defined('RCUBE_LIB_DIR')) { - define('RCUBE_LIB_DIR', dirname(__FILE__).DIRECTORY_SEPARATOR); + define('RCUBE_LIB_DIR', dirname(__FILE__) . '/'); } if (!defined('RCUBE_INSTALL_PATH')) { @@ -464,16 +464,14 @@ function rcube_autoload($classname) '/Net_(.+)/', '/Auth_(.+)/', '/^html_.+/', - '/^rcube(.*)/', - '/^utf8$/', + '/^rcube(.*)/' ), array( 'Mail/\\1', 'Net/\\1', 'Auth/\\1', 'Roundcube/html', - 'Roundcube/rcube\\1', - 'utf8.class', + 'Roundcube/rcube\\1' ), $classname ); diff --git a/program/lib/Roundcube/html.php b/program/lib/Roundcube/html.php index a88570d75..f18cad0bf 100644 --- a/program/lib/Roundcube/html.php +++ b/program/lib/Roundcube/html.php @@ -32,7 +32,7 @@ class html public static $doctype = 'xhtml'; public static $lc_tags = true; - public static $common_attrib = array('id','class','style','title','align','unselectable'); + public static $common_attrib = array('id','class','style','title','align','unselectable','tabindex','role'); public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script'); @@ -153,7 +153,7 @@ class html $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'))); + array('src','alt','width','height','border','usemap','onclick','onerror'))); } /** @@ -218,7 +218,7 @@ class html $attr = array('src' => $attr); } return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib, - array('src','name','width','height','border','frameborder','onload'))); + array('src','name','width','height','border','frameborder','onload','allowfullscreen'))); } /** @@ -283,9 +283,11 @@ class html continue; } - // ignore not allowed attributes, except data-* + // ignore not allowed attributes, except aria-* and data-* if (!empty($allowed)) { - if (!isset($allowed_f[$key]) && @substr_compare($key, 'data-', 0, 5) !== 0) { + $is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0; + $is_aria_attr = @substr_compare($key, 'aria-', 0, 5) === 0; + if (!$is_aria_attr && !$is_data_attr && !isset($allowed_f[$key])) { continue; } } @@ -835,7 +837,7 @@ class html_table extends html if (!empty($this->header)) { $rowcontent = ''; foreach ($this->header as $c => $col) { - $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content); + $rowcontent .= self::tag($this->_head_tagname(), $col->attrib, $col->content); } $thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) : self::tag($this->_row_tagname(), array('class' => 'thead'), $rowcontent, parent::$common_attrib); @@ -888,7 +890,16 @@ class html_table extends html private function _row_tagname() { static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div'); - return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*']; + return $row_tagnames[$this->tagname] ?: $row_tagnames['*']; + } + + /** + * Getter for the corresponding tag name for table row elements + */ + private function _head_tagname() + { + static $head_tagnames = array('table' => 'th', '*' => 'span'); + return $head_tagnames[$this->tagname] ?: $head_tagnames['*']; } /** @@ -897,7 +908,7 @@ class html_table extends html private function _col_tagname() { static $col_tagnames = array('table' => 'td', '*' => 'span'); - return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*']; + return $col_tagnames[$this->tagname] ?: $col_tagnames['*']; } } diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index f2aeda7f0..eedc46c7a 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -172,9 +172,13 @@ class rcube public function get_dbh() { if (!$this->db) { - $config_all = $this->config->all(); - $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']); - $this->db->set_debug((bool)$config_all['sql_debug']); + $this->db = rcube_db::factory( + $this->config->get('db_dsnw'), + $this->config->get('db_dsnr'), + $this->config->get('db_persistent') + ); + + $this->db->set_debug((bool)$this->config->get('sql_debug')); } return $this->db; @@ -357,15 +361,16 @@ class rcube // set class options $options = array( - 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), - 'auth_cid' => $this->config->get("{$driver}_auth_cid"), - 'auth_pw' => $this->config->get("{$driver}_auth_pw"), - 'debug' => (bool) $this->config->get("{$driver}_debug"), - 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), - 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"), - 'timeout' => (int) $this->config->get("{$driver}_timeout"), - 'skip_deleted' => (bool) $this->config->get('skip_deleted'), - 'driver' => $driver, + 'auth_type' => $this->config->get("{$driver}_auth_type", 'check'), + 'auth_cid' => $this->config->get("{$driver}_auth_cid"), + 'auth_pw' => $this->config->get("{$driver}_auth_pw"), + 'debug' => (bool) $this->config->get("{$driver}_debug"), + 'force_caps' => (bool) $this->config->get("{$driver}_force_caps"), + 'disabled_caps' => $this->config->get("{$driver}_disabled_caps"), + 'socket_options' => $this->config->get("{$driver}_conn_options"), + 'timeout' => (int) $this->config->get("{$driver}_timeout"), + 'skip_deleted' => (bool) $this->config->get('skip_deleted'), + 'driver' => $driver, ); if (!empty($_SESSION['storage_host'])) { @@ -384,8 +389,12 @@ class rcube $this->storage->set_options($options); $this->set_storage_prop(); - } + // subscribe to 'storage_connected' hook for session logging + if ($this->config->get('imap_log_session', false)) { + $this->plugins->register_hook('storage_connected', array($this, 'storage_log_session')); + } + } /** * Set storage parameters. @@ -453,6 +462,16 @@ class rcube /** + * Callback for IMAP connection events to log session identifiers + */ + public function storage_log_session($args) + { + if (!empty($args['session']) && session_id()) { + $this->write_log('imap_session', $args['session']); + } + } + + /** * Create session object and start the session. */ public function session_init() @@ -824,7 +843,13 @@ class rcube */ $clear = pack("a*H2", $clear, "80"); - if (function_exists('mcrypt_module_open') && + if (function_exists('openssl_encrypt')) { + $method = 'DES-EDE3-CBC'; + $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true; + $iv = $this->create_iv(openssl_cipher_iv_length($method)); + $cipher = $iv . openssl_encrypt($clear, $method, $ckey, $opts, $iv); + } + else if (function_exists('mcrypt_module_open') && ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) ) { $iv = $this->create_iv(mcrypt_enc_get_iv_size($td)); @@ -845,7 +870,7 @@ class rcube self::raise_error(array( 'code' => 500, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available" + 'message' => "Could not perform encryption; make sure OpenSSL or Mcrypt or lib/des.inc is available" ), true, true); } } @@ -871,7 +896,21 @@ class rcube $cipher = $base64 ? base64_decode($cipher) : $cipher; - if (function_exists('mcrypt_module_open') && + if (function_exists('openssl_decrypt')) { + $method = 'DES-EDE3-CBC'; + $opts = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true; + $iv_size = openssl_cipher_iv_length($method); + $iv = substr($cipher, 0, $iv_size); + + // session corruption? (#1485970) + if (strlen($iv) < $iv_size) { + return ''; + } + + $cipher = substr($cipher, $iv_size); + $clear = openssl_decrypt($cipher, $method, $ckey, $opts, $iv); + } + else if (function_exists('mcrypt_module_open') && ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, "")) ) { $iv_size = mcrypt_enc_get_iv_size($td); @@ -1113,8 +1152,12 @@ class rcube $line = var_export($line, true); } - $date_format = self::$instance ? self::$instance->config->get('log_date_format') : null; - $log_driver = self::$instance ? self::$instance->config->get('log_driver') : null; + $date_format = $log_driver = $session_key = null; + if (self::$instance) { + $date_format = self::$instance->config->get('log_date_format'); + $log_driver = self::$instance->config->get('log_driver'); + $session_key = intval(self::$instance->config->get('log_session_id', 8)); + } if (empty($date_format)) { $date_format = 'd-M-Y H:i:s O'; @@ -1132,6 +1175,11 @@ class rcube return true; } + // add session ID to the log + if ($session_key > 0 && ($sess = session_id())) { + $line = '<' . substr($sess, 0, $session_key) . '> ' . $line; + } + if ($log_driver == 'syslog') { $prio = $name == 'errors' ? LOG_ERR : LOG_INFO; syslog($prio, $line); diff --git a/program/lib/Roundcube/rcube_addressbook.php b/program/lib/Roundcube/rcube_addressbook.php index 4d9fa3db1..69027b0e8 100644 --- a/program/lib/Roundcube/rcube_addressbook.php +++ b/program/lib/Roundcube/rcube_addressbook.php @@ -557,6 +557,62 @@ abstract class rcube_addressbook } /** + * Build contact display name for autocomplete listing + * + * @param array Hash array with contact data as key-value pairs + * @param string Optional email address + * @param string Optional name (self::compose_list_name() result) + * @param string Optional template to use (defaults to the 'contact_search_name' config option) + * + * @return string Display name + */ + public static function compose_search_name($contact, $email = null, $name = null, $templ = null) + { + static $template; + + if (empty($templ) && !isset($template)) { // cache this + $template = rcube::get_instance()->config->get('contact_search_name'); + if (empty($template)) { + $template = '{name} <{email}>'; + } + } + + $result = $templ ?: $template; + + if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) { + foreach ($matches[0] as $key) { + $key = trim($key, '{}'); + $value = ''; + + switch ($key) { + case 'name': + $value = $name ?: self::compose_list_name($contact); + break; + + case 'email': + $value = $email; + break; + } + + if (empty($value)) { + $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true); + if (is_array($value)) { + $value = $value[0]; + } + } + + $result = str_replace('{' . $key . '}', $value, $result); + } + } + + $result = preg_replace('/\s+/', ' ', $result); + $result = preg_replace('/\s*(<>|\(\)|\[\])/', '', $result); + $result = trim($result, '/ '); + + return $result; + } + + /** * Create a unique key for sorting contacts */ public static function compose_contact_key($contact, $sort_col) diff --git a/program/lib/Roundcube/rcube_cache.php b/program/lib/Roundcube/rcube_cache.php index a708cb292..0017dcacc 100644 --- a/program/lib/Roundcube/rcube_cache.php +++ b/program/lib/Roundcube/rcube_cache.php @@ -45,6 +45,7 @@ class rcube_cache private $cache = array(); private $cache_changes = array(); private $cache_sums = array(); + private $max_packet = -1; /** @@ -319,7 +320,7 @@ class rcube_cache * Writes single cache record into DB. * * @param string $key Cache key name - * @param mxied $data Serialized cache data + * @param mixed $data Serialized cache data * * @param boolean True on success, False on failure */ @@ -329,6 +330,12 @@ class rcube_cache return false; } + // don't attempt to write too big data sets + if (strlen($data) > $this->max_packet_size()) { + trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING); + return false; + } + if ($this->type == 'memcache' || $this->type == 'apc') { return $this->add_record($this->ckey($key), $data); } @@ -591,4 +598,30 @@ class rcube_cache return $this->packed ? @unserialize($data) : $data; } + + /** + * Determine the maximum size for cache data to be written + */ + private function max_packet_size() + { + if ($this->max_packet < 0) { + $this->max_packet = 2097152; // default/max is 2 MB + + if ($this->type == 'db') { + $value = $this->db->get_variable('max_allowed_packet', 1048500); + $this->max_packet = min($value, $this->max_packet) - 2000; + } + else if ($this->type == 'memcache') { + $stats = $this->db->getStats(); + $remaining = $stats['limit_maxbytes'] - $stats['bytes']; + $this->max_packet = min($remaining / 5, $this->max_packet); + } + else if ($this->type == 'apc' && function_exists('apc_sma_info')) { + $stats = apc_sma_info(); + $this->max_packet = min($stats['avail_mem'] / 5, $this->max_packet); + } + } + + return $this->max_packet; + } } diff --git a/program/lib/Roundcube/rcube_charset.php b/program/lib/Roundcube/rcube_charset.php index ffec67376..d6ca3c052 100644 --- a/program/lib/Roundcube/rcube_charset.php +++ b/program/lib/Roundcube/rcube_charset.php @@ -273,17 +273,8 @@ class rcube_charset else if ($from == 'ISO-8859-1' && function_exists('utf8_encode')) { return utf8_encode($str); } - else if (class_exists('utf8')) { - if (!$conv) { - $conv = new utf8($from); - } - else { - $conv->loadCharset($from); - } - - if ($_str = $conv->strToUtf8($str)) { - return $_str; - } + else { + user_error("No suitable function found for UTF-8 encoding", E_USER_WARNING); } } @@ -298,17 +289,8 @@ class rcube_charset else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) { return utf8_decode($str); } - else if (class_exists('utf8')) { - if (!$conv) { - $conv = new utf8($to); - } - else { - $conv->loadCharset($from); - } - - if ($_str = $conv->strToUtf8($str)) { - return $_str; - } + else { + user_error("No suitable function found for UTF-8 decoding", E_USER_WARNING); } } diff --git a/program/lib/Roundcube/rcube_contacts.php b/program/lib/Roundcube/rcube_contacts.php index d215760cf..5e1a40e5b 100644 --- a/program/lib/Roundcube/rcube_contacts.php +++ b/program/lib/Roundcube/rcube_contacts.php @@ -264,7 +264,7 @@ class rcube_contacts extends rcube_addressbook if ($read_vcard) $sql_arr = $this->convert_db_data($sql_arr); else { - $sql_arr['email'] = explode(self::SEPARATOR, $sql_arr['email']); + $sql_arr['email'] = $sql_arr['email'] ? explode(self::SEPARATOR, $sql_arr['email']) : array(); $sql_arr['email'] = array_map('trim', $sql_arr['email']); } diff --git a/program/lib/Roundcube/rcube_csv2vcard.php b/program/lib/Roundcube/rcube_csv2vcard.php index 06bc387d5..b7d159178 100644 --- a/program/lib/Roundcube/rcube_csv2vcard.php +++ b/program/lib/Roundcube/rcube_csv2vcard.php @@ -149,6 +149,13 @@ class rcube_csv2vcard // GMail 'groups' => 'groups', + 'group_membership' => 'groups', + 'given_name' => 'firstname', + 'additional_name' => 'middlename', + 'family_name' => 'surname', + 'name' => 'displayname', + 'name_prefix' => 'prefix', + 'name_suffix' => 'suffix', ); /** @@ -272,12 +279,95 @@ class rcube_csv2vcard 'work_mobile' => "Work Mobile", 'work_title' => "Work Title", 'work_zip' => "Work Zip", - 'groups' => "Group", + 'group' => "Group", + + // GMail + 'groups' => "Groups", + 'group_membership' => "Group Membership", + 'given_name' => "Given Name", + 'additional_name' => "Additional Name", + 'family_name' => "Family Name", + 'name' => "Name", + 'name_prefix' => "Name Prefix", + 'name_suffix' => "Name Suffix", + ); + + /** + * Special fields map for GMail format + * + * @var array + */ + protected $gmail_label_map = array( + 'E-mail' => array( + 'Value' => array( + 'home' => 'email:home', + 'work' => 'email:work', + ), + ), + 'Phone' => array( + 'Value' => array( + 'home' => 'phone:home', + 'homefax' => 'phone:homefax', + 'main' => 'phone:pref', + 'pager' => 'phone:pager', + 'mobile' => 'phone:cell', + 'work' => 'phone:work', + 'workfax' => 'phone:workfax', + ), + ), + 'Relation' => array( + 'Value' => array( + 'spouse' => 'spouse', + ), + ), + 'Website' => array( + 'Value' => array( + 'profile' => 'website:profile', + 'blog' => 'website:blog', + 'homepage' => 'website:homepage', + 'work' => 'website:work', + ), + ), + 'Address' => array( + 'Street' => array( + 'home' => 'street:home', + 'work' => 'street:work', + ), + 'City' => array( + 'home' => 'locality:home', + 'work' => 'locality:work', + ), + 'Region' => array( + 'home' => 'region:home', + 'work' => 'region:work', + ), + 'Postal Code' => array( + 'home' => 'zipcode:home', + 'work' => 'zipcode:work', + ), + 'Country' => array( + 'home' => 'country:home', + 'work' => 'country:work', + ), + ), + 'Organization' => array( + 'Name' => array( + '' => 'organization', + ), + 'Title' => array( + '' => 'jobtitle', + ), + 'Department' => array( + '' => 'department', + ), + ), ); + protected $local_label_map = array(); - protected $vcards = array(); - protected $map = array(); + protected $vcards = array(); + protected $map = array(); + protected $gmail_map = array(); /** @@ -308,16 +398,24 @@ class rcube_csv2vcard public function import($csv) { // convert to UTF-8 - $head = substr($csv, 0, 4096); - $charset = rcube_charset::detect($head, RCUBE_CHARSET); - $csv = rcube_charset::convert($csv, $charset); - $head = ''; + $head = substr($csv, 0, 4096); + $charset = rcube_charset::detect($head, RCUBE_CHARSET); + $csv = rcube_charset::convert($csv, $charset); + $csv = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM + $head = ''; + $prev_line = false; - $this->map = array(); + $this->map = array(); + $this->gmail_map = array(); // Parse file foreach (preg_split("/[\r\n]+/", $csv) as $line) { + if (!empty($prev_line)) { + $line = '"' . $line; + } + $elements = $this->parse_line($line); + if (empty($elements)) { continue; } @@ -331,7 +429,28 @@ class rcube_csv2vcard } // Parse data row else { + // handle multiline elements (e.g. Gmail) + if (!empty($prev_line)) { + $first = array_shift($elements); + + if ($first[0] == '"') { + $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1); + } + else { + $prev_line[count($prev_line)-1] .= "\n" . $first; + } + + $elements = array_merge($prev_line, $elements); + } + + $last_element = $elements[count($elements)-1]; + if ($last_element[0] == '"') { + $elements[count($elements)-1] = substr($last_element, 1); + $prev_line = $elements; + continue; + } $this->csv_to_vcard($elements); + $prev_line = false; } } } @@ -389,6 +508,7 @@ class rcube_csv2vcard $map1[$i] = $this->csv2vcard_map[$label]; } } + // check localized labels if (!empty($this->local_label_map)) { for ($i = 0; $i < $size; $i++) { @@ -406,6 +526,22 @@ class rcube_csv2vcard } $this->map = count($map1) >= count($map2) ? $map1 : $map2; + + // support special Gmail format + foreach ($this->gmail_label_map as $key => $items) { + $num = 1; + while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) { + $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found); + foreach (array_keys($items) as $item_key) { + $_key = "$key $num - $item_key"; + if (($found = array_search($_key, $elements)) !== false) { + $this->gmail_map["$key:$num"][$item_key] = $found; + } + } + + $num++; + } + } } /** @@ -421,6 +557,22 @@ class rcube_csv2vcard } } + // Gmail format support + foreach ($this->gmail_map as $idx => $item) { + $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']])); + $key = $item['_key']; + + unset($item['_idx']); + unset($item['_key']); + + foreach ($item as $item_key => $item_idx) { + $value = $data[$item_idx]; + if ($value !== null && $value !== '' && ($data_idx = $this->gmail_label_map[$key][$item_key][$type])) { + $contact[$data_idx] = $value; + } + } + } + if (empty($contact)) { return; } @@ -430,9 +582,14 @@ class rcube_csv2vcard $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d']; } - // categories/groups separator in vCard is ',' not ';' if (!empty($contact['groups'])) { + // categories/groups separator in vCard is ',' not ';' $contact['groups'] = str_replace(';', ',', $contact['groups']); + + // remove "* " added by GMail + if (!empty($this->gmail_map)) { + $contact['groups'] = str_replace('* ', '', $contact['groups']); + } } // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00" diff --git a/program/lib/Roundcube/rcube_html2text.php b/program/lib/Roundcube/rcube_html2text.php index 8628371d7..499c4b05c 100644 --- a/program/lib/Roundcube/rcube_html2text.php +++ b/program/lib/Roundcube/rcube_html2text.php @@ -423,7 +423,7 @@ class rcube_html2text // Variables used for building the link list $this->_link_list = array(); - $text = trim(stripslashes($this->html)); + $text = $this->html; // Convert HTML to TXT $this->_converter($text); diff --git a/program/lib/Roundcube/rcube_image.php b/program/lib/Roundcube/rcube_image.php index a15368a7e..d0d0c7437 100644 --- a/program/lib/Roundcube/rcube_image.php +++ b/program/lib/Roundcube/rcube_image.php @@ -189,7 +189,7 @@ class rcube_image } else if($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) { $image = imagecreatefromgif($this->image_file); - $type = 'gid'; + $type = 'gif'; } else if($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) { $image = imagecreatefrompng($this->image_file); @@ -228,6 +228,24 @@ class rcube_image imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']); $image = $new_image; + // fix rotation of image if EXIF data exists and specifies rotation (GD strips the EXIF data) + if ($this->image_file && function_exists('exif_read_data')) { + $exif = exif_read_data($this->image_file); + if ($exif && $exif['Orientation']) { + switch ($exif['Orientation']) { + case 3: + $image = imagerotate($image, 180, 0); + break; + case 6: + $image = imagerotate($image, -90, 0); + break; + case 8: + $image = imagerotate($image, 90, 0); + break; + } + } + } + if ($props['gd_type'] == IMAGETYPE_JPEG) { $result = imagejpeg($image, $filename, 75); } diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 4204354b3..a66d2064d 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -56,6 +56,7 @@ class rcube_imap extends rcube_storage */ protected $icache = array(); + protected $plugins; protected $list_page = 1; protected $delimiter; protected $namespace; @@ -82,6 +83,7 @@ class rcube_imap extends rcube_storage public function __construct() { $this->conn = new rcube_imap_generic(); + $this->plugins = rcube::get_instance()->plugins; // Set namespace and delimiter from session, // so some methods would work before connection @@ -110,13 +112,13 @@ class rcube_imap extends rcube_storage /** * Connect to an IMAP server * - * @param string $host Host to connect - * @param string $user Username for IMAP account - * @param string $pass Password for IMAP account - * @param integer $port Port to connect to - * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection + * @param string $host Host to connect + * @param string $user Username for IMAP account + * @param string $pass Password for IMAP account + * @param integer $port Port to connect to + * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection * - * @return boolean TRUE on success, FALSE on failure + * @return boolean True on success, False on failure */ public function connect($host, $user, $pass, $port=143, $use_ssl=null) { @@ -147,7 +149,7 @@ class rcube_imap extends rcube_storage $attempt = 0; do { - $data = rcube::get_instance()->plugins->exec_hook('storage_connect', + $data = $this->plugins->exec_hook('storage_connect', array_merge($this->options, array('host' => $host, 'user' => $user, 'attempt' => ++$attempt))); @@ -170,8 +172,20 @@ class rcube_imap extends rcube_storage $this->connect_done = true; if ($this->conn->connected()) { + // check for session identifier + $session = null; + if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) { + $session = $m[1]; + } + // get namespace and delimiter $this->set_env(); + + // trigger post-connect hook + $this->plugins->exec_hook('storage_connected', array( + 'host' => $host, 'user' => $user, 'session' => $session + )); + return true; } // write error log @@ -761,7 +775,7 @@ class rcube_imap extends rcube_storage $page = $page ? $page : $this->list_page; // use saved message set - if ($this->search_string && $folder == $this->folder) { + if ($this->search_string) { return $this->list_search_messages($folder, $page, $slice); } @@ -1489,23 +1503,39 @@ class rcube_imap extends rcube_storage * Invoke search request to IMAP server * * @param string $folder Folder name to search in - * @param string $str Search criteria + * @param string $search Search criteria * @param string $charset Search charset * @param string $sort_field Header field to sort by + * * @return rcube_result_index Search result object * @todo: Search criteria should be provided in non-IMAP format, eg. array */ - public function search($folder='', $str='ALL', $charset=NULL, $sort_field=NULL) + public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null) { - if (!$str) { - $str = 'ALL'; + if (!$search) { + $search = 'ALL'; } - // multi-folder search - if (is_array($folder) && count($folder) > 1 && $str != 'ALL') { - new rcube_result_index; // trigger autoloader and make these classes available for threaded context - new rcube_result_thread; + if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) { + $folder = $this->folder; + } + + $plugin = $this->plugins->exec_hook('imap_search_before', array( + 'folder' => $folder, + 'search' => $search, + 'charset' => $charset, + 'sort_field' => $sort_field, + 'threading' => $this->threading, + )); + $folder = $plugin['folder']; + $search = $plugin['search']; + $charset = $plugin['charset']; + $sort_field = $plugin['sort_field']; + $results = $plugin['result']; + + // multi-folder search + if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') { // connect IMAP to have all the required classes and settings loaded $this->check_connection(); @@ -1518,29 +1548,28 @@ class rcube_imap extends rcube_storage $searcher->set_timelimit(60); // continue existing incomplete search - if (!empty($this->search_set) && $this->search_set->incomplete && $str == $this->search_string) { + if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) { $searcher->set_results($this->search_set); } // execute the search $results = $searcher->exec( $folder, - $str, + $search, $charset ? $charset : $this->default_charset, $sort_field && $this->get_capability('SORT') ? $sort_field : null, $this->threading ); } - else { - $folder = is_array($folder) ? $folder[0] : $folder; - if (!strlen($folder)) { - $folder = $this->folder; - } - $results = $this->search_index($folder, $str, $charset, $sort_field); + else if (!$results) { + $folder = is_array($folder) ? $folder[0] : $folder; + $search = is_array($search) ? $search[$folder] : $search; + $results = $this->search_index($folder, $search, $charset, $sort_field); } - $this->set_search_set(array($str, $results, $charset, $sort_field, - $this->threading || $this->search_sorted ? true : false)); + $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false; + + $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted)); return $results; } @@ -1676,12 +1705,15 @@ class rcube_imap extends rcube_storage $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n $string = substr($str, $string_offset - 1, $m[0]); $string = rcube_charset::convert($string, $charset, $dest_charset); - if ($string === false) { + + if ($string === false || !strlen($string)) { continue; } + $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string); $last = $m[0] + $string_offset - 1; } + if ($last < strlen($str)) { $res .= substr($str, $last, strlen($str)-$last); } @@ -1830,7 +1862,7 @@ class rcube_imap extends rcube_storage $this->struct_charset = $this->structure_charset($structure); } - $headers->ctype = strtolower($headers->ctype); + $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 @@ -2483,7 +2515,7 @@ class rcube_imap extends rcube_storage // increase messagecount of the target folder $this->set_messagecount($folder, 'ALL', 1); - rcube::get_instance()->plugins->exec_hook('message_saved', array( + $this->plugins->exec_hook('message_saved', array( 'folder' => $folder, 'message' => $message, 'headers' => $headers, @@ -2759,7 +2791,7 @@ class rcube_imap extends rcube_storage } // Give plugins a chance to provide a list of folders - $data = rcube::get_instance()->plugins->exec_hook('storage_folders', + $data = $this->plugins->exec_hook('storage_folders', array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB')); if (isset($data['folders'])) { @@ -2891,7 +2923,7 @@ class rcube_imap extends rcube_storage } // Give plugins a chance to provide a list of folders - $data = rcube::get_instance()->plugins->exec_hook('storage_folders', + $data = $this->plugins->exec_hook('storage_folders', array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST')); if (isset($data['folders'])) { @@ -2972,7 +3004,7 @@ class rcube_imap extends rcube_storage * @param array $result Reference to folders list * @param string $type Listing type (ext-subscribed, subscribed or all) */ - private function list_folders_update(&$result, $type = null) + protected function list_folders_update(&$result, $type = null) { $namespace = $this->get_namespace(); $search = array(); @@ -3049,14 +3081,15 @@ class rcube_imap extends rcube_storage /** * Get mailbox quota information - * added by Nuny + * + * @param string $folder Folder name * * @return mixed Quota info or False if not supported */ - public function get_quota() + public function get_quota($folder = null) { if ($this->get_capability('QUOTA') && $this->check_connection()) { - return $this->conn->getQuota(); + return $this->conn->getQuota($folder); } return false; @@ -3132,6 +3165,16 @@ class rcube_imap extends rcube_storage $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null); + // it's quite often situation that we're trying to create and subscribe + // a folder that already exist, but is unsubscribed + if (!$result) { + if ($this->get_response_code() == rcube_storage::ALREADYEXISTS + || preg_match('/already exists/i', $this->get_error_str()) + ) { + $result = true; + } + } + // try to subscribe it if ($result) { // clear cache @@ -3279,12 +3322,14 @@ class rcube_imap extends rcube_storage // request \Subscribed flag in LIST response as performance improvement for folder_exists() $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE')); - foreach ($folders as $folder) { - if ($flags = $this->conn->data['LIST'][$folder]) { - foreach ($types as $type) { - if (in_array($type, $flags)) { - $type = strtolower(substr($type, 1)); - $special[$type] = $folder; + if (!empty($folders)) { + foreach ($folders as $folder) { + if ($flags = $this->conn->data['LIST'][$folder]) { + foreach ($types as $type) { + if (in_array($type, $flags)) { + $type = strtolower(substr($type, 1)); + $special[$type] = $folder; + } } } } @@ -4124,61 +4169,82 @@ class rcube_imap extends rcube_storage */ public function sort_folder_list($a_folders, $skip_default = false) { - $a_out = $a_defaults = $folders = array(); - - $delimiter = $this->get_hierarchy_delimiter(); $specials = array_merge(array('INBOX'), array_values($this->get_special_folders())); + $folders = array(); - // find default folders and skip folders starting with '.' + // convert names to UTF-8 and skip folders starting with '.' foreach ($a_folders as $folder) { - if ($folder[0] == '.') { - continue; - } - - if (!$skip_default && ($p = array_search($folder, $specials)) !== false && !$a_defaults[$p]) { - $a_defaults[$p] = $folder; - } - else { - $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP'); + if ($folder[0] != '.') { + // for better performance skip encoding conversion + // if the string does not look like UTF7-IMAP + $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); } } - // sort folders and place defaults on the top - asort($folders, SORT_LOCALE_STRING); - ksort($a_defaults); - $folders = array_merge($a_defaults, array_keys($folders)); + // sort folders + // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names + uasort($folders, array($this, 'sort_folder_comparator')); + + $folders = 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); + if ($skip_default) { + return $folders; } - return $a_out; - } + // force the type of folder name variable (#1485527) + $folders = array_map('strval', $folders); + $out = array(); + + // finally we must put special folders on top and rebuild the list + // to move their subfolders where they belong... + $specials = array_unique(array_intersect($specials, $folders)); + $folders = array_merge($specials, array_diff($folders, $specials)); + + $this->sort_folder_specials(null, $folders, $specials, $out); + return $out; + } /** - * Recursive method for sorting folders + * Recursive function to put subfolders of special folders in place */ - protected function rsort($folder, $delimiter, &$list, &$out) + protected function sort_folder_specials($folder, &$list, &$specials, &$out) { while (list($key, $name) = each($list)) { - if (strpos($name, $folder.$delimiter) === 0) { - // set the type of folder name variable (#1485527) - $out[] = (string) $name; + if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) { + $out[] = $name; unset($list[$key]); - $this->rsort($name, $delimiter, $list, $out); + + if (!empty($specials) && ($found = array_search($name, $specials)) !== false) { + unset($specials[$found]); + $this->sort_folder_specials($name, $list, $specials, $out); + } } } + reset($list); } + /** + * Callback for uasort() that implements correct + * locale-aware case-sensitive sorting + */ + protected function sort_folder_comparator($str1, $str2) + { + $path1 = explode($this->delimiter, $str1); + $path2 = explode($this->delimiter, $str2); + + foreach ($path1 as $idx => $folder1) { + $folder2 = $path2[$idx]; + + if ($folder1 === $folder2) { + continue; + } + + return strcoll($folder1, $folder2); + } + } + /** * Find UID of the specified message sequence ID diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index f45694dd0..68d9c6a2c 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -50,17 +50,17 @@ class rcube_imap_generic public static $mupdate; - private $fp; - private $host; - private $logged = false; - private $capability = array(); - private $capability_readed = false; - private $prefs; - private $cmd_tag; - private $cmd_num = 0; - private $resourceid; - private $_debug = false; - private $_debug_handler = false; + protected $fp; + protected $host; + protected $logged = false; + protected $capability = array(); + protected $capability_readed = false; + protected $prefs; + protected $cmd_tag; + protected $cmd_num = 0; + protected $resourceid; + protected $_debug = false; + protected $_debug_handler = false; const ERROR_OK = 0; const ERROR_NO = -1; @@ -352,7 +352,7 @@ class rcube_imap_generic * * @return bool True if connection is closed */ - private function eof() + protected function eof() { if (!is_resource($this->fp)) { return true; @@ -375,7 +375,7 @@ class rcube_imap_generic /** * Closes connection stream. */ - private function closeSocket() + protected function closeSocket() { @fclose($this->fp); $this->fp = null; @@ -421,7 +421,7 @@ class rcube_imap_generic return false; } - private function hasCapability($name) + protected function hasCapability($name) { if (empty($this->capability) || $name == '') { return false; @@ -723,110 +723,38 @@ class rcube_imap_generic // configure $this->set_prefs($options); - $auth_method = $this->prefs['auth_type']; - $result = false; - - // initialize connection - $this->error = ''; - $this->errornum = self::ERROR_OK; - $this->selected = null; - $this->user = $user; $this->host = $host; + $this->user = $user; $this->logged = false; + $this->selected = null; // check input if (empty($host)) { $this->setError(self::ERROR_BAD, "Empty host"); return false; } + if (empty($user)) { $this->setError(self::ERROR_NO, "Empty user"); return false; } + if (empty($password)) { $this->setError(self::ERROR_NO, "Empty password"); return false; } - if (!$this->prefs['port']) { - $this->prefs['port'] = 143; - } - // check for SSL - if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') { - $host = $this->prefs['ssl_mode'] . '://' . $host; - } - - if ($this->prefs['timeout'] <= 0) { - $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout'))); - } - // Connect - $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); - - if (!$this->fp) { - if (!$errstr) { - $errstr = "Unknown reason (fsockopen() function disabled?)"; - } - $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr)); - return false; - } - - if ($this->prefs['timeout'] > 0) { - stream_set_timeout($this->fp, $this->prefs['timeout']); - } - - $line = trim(fgets($this->fp, 8192)); - - if ($this->_debug) { - // set connection identifier for debug output - preg_match('/#([0-9]+)/', (string)$this->fp, $m); - $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4)); - - if ($line) - $this->debug('S: '. $line); - } - - // Connected to wrong port or connection error? - if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { - if ($line) - $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); - else - $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); - - $this->setError(self::ERROR_BAD, $error); - $this->closeConnection(); + if (!$this->_connect($host)) { return false; } - // RFC3501 [7.1] optional CAPABILITY response - if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { - $this->parseCapability($matches[1], true); - } - - // TLS connection - if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { - $res = $this->execute('STARTTLS'); - - if ($res[0] != self::ERROR_OK) { - $this->closeConnection(); - return false; - } - - if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { - $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); - $this->closeConnection(); - return false; - } - - // Now we're secure, capabilities need to be reread - $this->clearCapability(); - } - // Send ID info if (!empty($this->prefs['ident']) && $this->getCapability('ID')) { $this->id($this->prefs['ident']); } + $auth_method = $this->prefs['auth_type']; $auth_methods = array(); $result = null; @@ -901,6 +829,103 @@ class rcube_imap_generic } /** + * Connects to IMAP server. + * + * @param string $host Server hostname or IP + * + * @return bool True on success, False on failure + */ + protected function _connect($host) + { + // initialize connection + $this->error = ''; + $this->errornum = self::ERROR_OK; + + if (!$this->prefs['port']) { + $this->prefs['port'] = 143; + } + + // check for SSL + if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') { + $host = $this->prefs['ssl_mode'] . '://' . $host; + } + + if ($this->prefs['timeout'] <= 0) { + $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout'))); + } + + if (!empty($this->prefs['socket_options'])) { + $context = stream_context_create($this->prefs['socket_options']); + $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr, + $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context); + } + else { + $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']); + } + + if (!$this->fp) { + $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", + $host, $this->prefs['port'], $errstr ?: "Unknown reason")); + + return false; + } + + if ($this->prefs['timeout'] > 0) { + stream_set_timeout($this->fp, $this->prefs['timeout']); + } + + $line = trim(fgets($this->fp, 8192)); + + if ($this->_debug) { + // set connection identifier for debug output + preg_match('/#([0-9]+)/', (string) $this->fp, $m); + $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4)); + + if ($line) { + $this->debug('S: '. $line); + } + } + + // Connected to wrong port or connection error? + if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) { + if ($line) + $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line); + else + $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']); + + $this->setError(self::ERROR_BAD, $error); + $this->closeConnection(); + return false; + } + + // RFC3501 [7.1] optional CAPABILITY response + if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) { + $this->parseCapability($matches[1], true); + } + + // TLS connection + if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) { + $res = $this->execute('STARTTLS'); + + if ($res[0] != self::ERROR_OK) { + $this->closeConnection(); + return false; + } + + if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { + $this->setError(self::ERROR_BAD, "Unable to negotiate TLS"); + $this->closeConnection(); + return false; + } + + // Now we're secure, capabilities need to be reread + $this->clearCapability(); + } + + return true; + } + + /** * Initializes environment */ protected function set_prefs($prefs) @@ -1268,15 +1293,15 @@ class rcube_imap_generic * * @param string $ref Reference name * @param string $mailbox Mailbox name - * @param array $status_opts (see self::_listMailboxes) + * @param array $return_opts (see self::_listMailboxes) * @param array $select_opts (see self::_listMailboxes) * - * @return array List of mailboxes or hash of options if $status_opts argument - * is non-empty. + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. */ - function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array()) + function listMailboxes($ref, $mailbox, $return_opts=array(), $select_opts=array()) { - return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts); + return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts); } /** @@ -1284,14 +1309,14 @@ class rcube_imap_generic * * @param string $ref Reference name * @param string $mailbox Mailbox name - * @param array $status_opts (see self::_listMailboxes) + * @param array $return_opts (see self::_listMailboxes) * - * @return array List of mailboxes or hash of options if $status_opts argument - * is non-empty. + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. */ - function listSubscribed($ref, $mailbox, $status_opts=array()) + function listSubscribed($ref, $mailbox, $return_opts=array()) { - return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL); + return $this->_listMailboxes($ref, $mailbox, true, $return_opts, NULL); } /** @@ -1300,18 +1325,18 @@ class rcube_imap_generic * @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: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN) - * or RETURN options (RFC5258: LIST_EXTENDED: SUBSCRIBED, CHILDREN) + * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED) + * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN, + * MYRIGHTS, SUBSCRIBED, CHILDREN * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED) * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE, * SPECIAL-USE (RFC6154) * - * @return array List of mailboxes or hash of options if $status_ops argument - * is non-empty. + * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response + * is requested, False on error. */ - private function _listMailboxes($ref, $mailbox, $subscribed=false, - $status_opts=array(), $select_opts=array()) + protected function _listMailboxes($ref, $mailbox, $subscribed=false, + $return_opts=array(), $select_opts=array()) { if (!strlen($mailbox)) { $mailbox = '*'; @@ -1329,17 +1354,25 @@ class rcube_imap_generic $args[] = $this->escape($ref); $args[] = $this->escape($mailbox); - if (!empty($status_opts) && $this->getCapability('LIST-EXTENDED')) { - $rets = array_intersect($status_opts, array('SUBSCRIBED', 'CHILDREN')); + if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) { + $ext_opts = array('SUBSCRIBED', 'CHILDREN'); + $rets = array_intersect($return_opts, $ext_opts); + $return_opts = array_diff($return_opts, $rets); } - if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) { - $status_opts = array_intersect($status_opts, array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN')); + if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) { + $lstatus = true; + $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'); + $opts = array_diff($return_opts, $status_opts); + $status_opts = array_diff($return_opts, $opts); if (!empty($status_opts)) { - $lstatus = true; $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')'; } + + if (!empty($opts)) { + $rets = array_merge($rets, $opts); + } } if (!empty($rets)) { @@ -1363,9 +1396,10 @@ class rcube_imap_generic $line = substr($response, $last, $pos - $last); $last = $pos + 2; - if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) { + if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) { continue; } + $cmd = strtoupper($m[1]); $line = substr($line, strlen($m[0])); @@ -1396,13 +1430,20 @@ class rcube_imap_generic $this->data['LIST'][$mailbox], $opts)); } } - // * STATUS <mailbox> (<result>) - else if ($cmd == 'STATUS') { - list($mailbox, $status) = $this->tokenizeResponse($line, 2); - - for ($i=0, $len=count($status); $i<$len; $i += 2) { - list($name, $value) = $this->tokenizeResponse($status, 2); - $folders[$mailbox][$name] = $value; + else if ($lstatus) { + // * STATUS <mailbox> (<result>) + if ($cmd == 'STATUS') { + list($mailbox, $status) = $this->tokenizeResponse($line, 2); + + for ($i=0, $len=count($status); $i<$len; $i += 2) { + list($name, $value) = $this->tokenizeResponse($status, 2); + $folders[$mailbox][$name] = $value; + } + } + // * MYRIGHTS <mailbox> <acl> + else if ($cmd == 'MYRIGHTS') { + list($mailbox, $acl) = $this->tokenizeResponse($line, 2); + $folders[$mailbox]['MYRIGHTS'] = $acl; } } } @@ -1877,8 +1918,8 @@ class rcube_imap_generic $result[$id] = ''; } } else if ($mode == 2) { - if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) { - $result[$id] = trim($matches[2]); + if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) { + $result[$id] = trim($matches[1]); } else { $result[$id] = 0; } @@ -1985,12 +2026,8 @@ class rcube_imap_generic * * @return bool True on success, False on failure */ - private function modFlag($mailbox, $messages, $flag, $mod = '+') + protected function modFlag($mailbox, $messages, $flag, $mod = '+') { - if ($mod != '+' && $mod != '-') { - $mod = '+'; - } - if (!$this->select($mailbox)) { return false; } @@ -2000,12 +2037,25 @@ class rcube_imap_generic return false; } + if ($this->flags[strtoupper($flag)]) { + $flag = $this->flags[strtoupper($flag)]; + } + + if (!$flag || (!in_array($flag, (array) $this->data['PERMANENTFLAGS']) + && !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])) + ) { + return false; + } + // Clear internal status cache if ($flag == 'SEEN') { unset($this->data['STATUS:'.$mailbox]['UNSEEN']); } - $flag = $this->flags[strtoupper($flag)]; + if ($mod != '+' && $mod != '-') { + $mod = '+'; + } + $result = $this->execute('UID STORE', array( $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"), self::COMMAND_NORESPONSE); @@ -2834,59 +2884,66 @@ class rcube_imap_generic /** * Returns QUOTA information * + * @param string $mailbox Mailbox name + * * @return array Quota information */ - function getQuota() - { - /* - * GETQUOTAROOT "INBOX" - * QUOTAROOT INBOX user/rchijiiwa1 - * QUOTA user/rchijiiwa1 (STORAGE 654 9765) - * OK Completed - */ - $result = false; - $quota_lines = array(); - $key = $this->nextTag(); - $command = $key . ' GETQUOTAROOT INBOX'; - - // get line(s) containing quota info - if ($this->putLine($command)) { - do { - $line = rtrim($this->readLine(5000)); - if (preg_match('/^\* QUOTA /', $line)) { - $quota_lines[] = $line; - } - } while (!$this->startsWith($line, $key, true, true)); - } - else { - $this->setError(self::ERROR_COMMAND, "Unable to send command: $command"); + function getQuota($mailbox = null) + { + if ($mailbox === null || $mailbox === '') { + $mailbox = 'INBOX'; } - // return false if not found, parse if found + // a0001 GETQUOTAROOT INBOX + // * QUOTAROOT INBOX user/sample + // * QUOTA user/sample (STORAGE 654 9765) + // a0001 OK Completed + + list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox))); + + $result = false; $min_free = PHP_INT_MAX; - foreach ($quota_lines as $key => $quota_line) { - $quota_line = str_replace(array('(', ')'), '', $quota_line); - $parts = explode(' ', $quota_line); - $storage_part = array_search('STORAGE', $parts); + $all = array(); - if (!$storage_part) { - continue; - } + if ($code == self::ERROR_OK) { + foreach (explode("\n", $response) as $line) { + if (preg_match('/^\* QUOTA /', $line)) { + list(, , $quota_root) = $this->tokenizeResponse($line, 3); + + while ($line) { + list($type, $used, $total) = $this->tokenizeResponse($line, 1); + $type = strtolower($type); - $used = intval($parts[$storage_part+1]); - $total = intval($parts[$storage_part+2]); - $free = $total - $used; + if ($type && $total) { + $all[$quota_root][$type]['used'] = intval($used); + $all[$quota_root][$type]['total'] = intval($total); + } + } + + if (empty($all[$quota_root]['storage'])) { + continue; + } - // 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']; + $used = $all[$quota_root]['storage']['used']; + $total = $all[$quota_root]['storage']['total']; + $free = $total - $used; + + // calculate lowest available space from all storage quotas + if ($free < $min_free) { + $min_free = $free; + $result['used'] = $used; + $result['total'] = $total; + $result['percent'] = min(100, round(($used/max(1,$total))*100)); + $result['free'] = 100 - $result['percent']; + } + } } } + if (!empty($result)) { + $result['all'] = $all; + } + return $result; } @@ -3143,7 +3200,8 @@ class rcube_imap_generic 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]; + if ($data[$i][$x+1] !== null) + $result[$mbox][$data[$i][$x]] = $data[$i][++$x]; } unset($data[$i]); } @@ -3161,7 +3219,8 @@ class rcube_imap_generic } } else if (isset($mbox)) { - $result[$mbox][$data[$i]] = $data[++$i]; + if ($data[$i+1] !== null) + $result[$mbox][$data[$i]] = $data[++$i]; unset($data[$i]); unset($data[$i-1]); } @@ -3306,10 +3365,10 @@ class rcube_imap_generic for ($x=0, $len=count($attribs); $x<$len;) { $attr = $attribs[$x++]; $value = $attribs[$x++]; - if ($attr == 'value.priv') { + if ($attr == 'value.priv' && $value !== null) { $result[$mbox]['/private' . $entry] = $value; } - else if ($attr == 'value.shared') { + else if ($attr == 'value.shared' && $value !== null) { $result[$mbox]['/shared' . $entry] = $value; } } @@ -3679,7 +3738,7 @@ class rcube_imap_generic return $result; } - private function _xor($string, $string2) + protected function _xor($string, $string2) { $result = ''; $size = strlen($string); @@ -3698,7 +3757,7 @@ class rcube_imap_generic * * @return string Space-separated list of flags */ - private function flagsToStr($flags) + protected function flagsToStr($flags) { foreach ((array)$flags as $idx => $flag) { if ($flag = $this->flags[strtoupper($flag)]) { @@ -3750,7 +3809,7 @@ class rcube_imap_generic /** * CAPABILITY response parser */ - private function parseCapability($str, $trusted=false) + protected function parseCapability($str, $trusted=false) { $str = preg_replace('/^\* CAPABILITY /i', '', $str); @@ -3827,7 +3886,7 @@ class rcube_imap_generic * * @since 0.5-stable */ - private function debug($message) + protected function debug($message) { if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) { $diff = $len - self::DEBUG_LINE_LENGTH; diff --git a/program/lib/Roundcube/rcube_imap_search.php b/program/lib/Roundcube/rcube_imap_search.php index 0c44daf1b..365d78f76 100644 --- a/program/lib/Roundcube/rcube_imap_search.php +++ b/program/lib/Roundcube/rcube_imap_search.php @@ -29,7 +29,7 @@ class rcube_imap_search { public $options = array(); - protected $jobs = array(); + protected $jobs = array(); protected $timelimit = 0; protected $results; protected $conn; @@ -40,7 +40,7 @@ class rcube_imap_search public function __construct($options, $conn) { $this->options = $options; - $this->conn = $conn; + $this->conn = $conn; } /** @@ -54,7 +54,7 @@ class rcube_imap_search */ public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null) { - $start = floor(microtime(true)); + $start = floor(microtime(true)); $results = new rcube_result_multifolder($folders); // start a search job for every folder to search in @@ -65,7 +65,8 @@ class rcube_imap_search $results->add($result); } else { - $job = new rcube_imap_search_job($folder, $str, $charset, $sort_field, $threading); + $search = is_array($str) && $str[$folder] ? $str[$folder] : $str; + $job = new rcube_imap_search_job($folder, $search, $charset, $sort_field, $threading); $job->worker = $this; $this->jobs[] = $job; } @@ -129,11 +130,11 @@ class rcube_imap_search_job /* extends Stackable */ public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false) { - $this->folder = $folder; - $this->search = $str; - $this->charset = $charset; + $this->folder = $folder; + $this->search = $str; + $this->charset = $charset; $this->sort_field = $sort_field; - $this->threading = $threading; + $this->threading = $threading; $this->result = new rcube_result_index($folder); $this->result->incomplete = true; @@ -150,9 +151,8 @@ class rcube_imap_search_job /* extends Stackable */ protected function search_index() { $criteria = $this->search; - $charset = $this->charset; - - $imap = $this->worker->get_imap(); + $charset = $this->charset; + $imap = $this->worker->get_imap(); if (!$imap->connected()) { trigger_error("No IMAP connection for $this->folder", E_USER_WARNING); @@ -228,7 +228,4 @@ class rcube_imap_search_job /* extends Stackable */ { return $this->result; } - } - - diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index 5a4b9dd61..6805c4902 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -65,6 +65,8 @@ class rcube_ldap extends rcube_addressbook private $base_dn = ''; private $groups_base_dn = ''; private $group_url; + private $group_data; + private $group_search_cache; private $cache; @@ -101,8 +103,8 @@ class rcube_ldap extends rcube_addressbook // add group name attrib to the list of attributes to be fetched $fetch_attributes[] = $this->prop['groups']['name_attr']; } - if (is_array($p['group_filters']) && count($p['group_filters'])) { - $this->groups = true; + if (is_array($p['group_filters'])) { + $this->groups = $this->groups || count($p['group_filters']); foreach ($p['group_filters'] as $k => $group_filter) { // set default name attribute to cn @@ -232,8 +234,7 @@ class rcube_ldap extends rcube_addressbook // initialize ldap wrapper object $this->ldap = new rcube_ldap_generic($this->prop); - $this->ldap->set_cache($this->cache); - $this->ldap->set_debug($this->debug); + $this->ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); $this->_connect(); } @@ -320,8 +321,7 @@ class rcube_ldap extends rcube_addressbook // we need to use a separate LDAP connection if (!empty($this->prop['vlv'])) { $ldap = new rcube_ldap_generic($this->prop); - $ldap->set_debug($this->debug); - $ldap->set_cache($this->cache); + $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); if (!$ldap->connect($host)) { continue; } @@ -335,7 +335,7 @@ class rcube_ldap extends rcube_addressbook $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs); if ($res) { $res->rewind(); - $replaces['%dn'] = $res->get_dn(); + $replaces['%dn'] = key($res->entries(TRUE)); // add more replacements from 'search_bind_attrib' config if ($search_bind_attrib) { @@ -383,7 +383,7 @@ class rcube_ldap extends rcube_addressbook $this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces); } - if (!empty($this->prop['group_filters'])) { + if (is_array($this->prop['group_filters'])) { foreach ($this->prop['group_filters'] as $i => $gf) { if (!empty($gf['base_dn'])) $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces); @@ -906,7 +906,6 @@ class rcube_ldap extends rcube_addressbook return $this->result; } - /** * Get a specific contact record * @@ -948,6 +947,23 @@ class rcube_ldap extends rcube_addressbook return $assoc ? $res : $this->result; } + /** + * Returns the last error occurred (e.g. when updating/inserting failed) + * + * @return array Hash array with the following fields: type, message + */ + function get_error() + { + $err = $this->error; + + // check ldap connection for errors + if (!$err && $this->ldap->get_error()) { + $err = array(self::ERROR_SEARCH, $this->ldap->get_error()); + } + + return $err; + } + /** * Check the given data before saving. @@ -1066,7 +1082,7 @@ class rcube_ldap extends rcube_addressbook } } - if (!$this->ldap->add($dn, $newentry)) { + if (!$this->ldap->add_entry($dn, $newentry)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1078,7 +1094,7 @@ class rcube_ldap extends rcube_addressbook 'objectClass' => (array) $this->prop['sub_fields'][$xidx], ); - $this->ldap->add($xdn, $xf); + $this->ldap->add_entry($xdn, $xf); } $dn = self::dn_encode($dn); @@ -1221,7 +1237,7 @@ class rcube_ldap extends rcube_addressbook if (!empty($subdeldata)) { foreach ($subdeldata as $fld => $val) { $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; - if (!$this->ldap->delete($subdn)) { + if (!$this->ldap->delete_entry($subdn)) { return false; } } @@ -1265,7 +1281,7 @@ class rcube_ldap extends rcube_addressbook $fld => $val, 'objectClass' => (array) $this->prop['sub_fields'][$fld], ); - $this->ldap->add($subdn, $xf); + $this->ldap->add_entry($subdn, $xf); } } @@ -1295,7 +1311,7 @@ class rcube_ldap extends rcube_addressbook if ($this->sub_filter) { if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) { foreach ($entries as $entry) { - if (!$this->ldap->delete($entry['dn'])) { + if (!$this->ldap->delete_entry($entry['dn'])) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1304,12 +1320,12 @@ class rcube_ldap extends rcube_addressbook } // Delete the record. - if (!$this->ldap->delete($dn)) { + if (!$this->ldap->delete_entry($dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } - // remove contact from all groups where he was member + // remove contact from all groups where he was a member if ($this->groups) { $dn = self::dn_encode($dn); $group_ids = $this->get_record_groups($dn); @@ -1342,7 +1358,7 @@ class rcube_ldap extends rcube_addressbook if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) { foreach ($groups as $group) { - $this->ldap->delete($group['dn']); + $this->ldap->delete_entry($group['dn']); } if ($this->cache) { @@ -1567,7 +1583,7 @@ class rcube_ldap extends rcube_addressbook $this->debug = $dbg; if ($this->ldap) { - $this->ldap->set_debug($dbg); + $this->ldap->config_set('debug', $dbg); } } @@ -1604,12 +1620,12 @@ class rcube_ldap extends rcube_addressbook return array(); } - $group_cache = $this->_fetch_groups(); + $group_cache = $this->_fetch_groups($search, $mode); $groups = array(); if ($search) { foreach ($group_cache as $group) { - if ($this->compare_search_value('name', $group['name'], $search, $mode)) { + if ($this->compare_search_value('name', $group['name'], mb_strtolower($search), $mode)) { $groups[] = $group; } } @@ -1624,10 +1640,19 @@ class rcube_ldap extends rcube_addressbook /** * Fetch groups from server */ - private function _fetch_groups($vlv_page = null) + private function _fetch_groups($search = null, $mode = 0, $vlv_page = null) { + // reset group search cache + if ($search !== null && $vlv_page === null) { + $this->group_search_cache = null; + } + // return in-memory cache from previous search results + else if (is_array($this->group_search_cache) && $vlv_page === null) { + return $this->group_search_cache; + } + // special case: list groups from 'group_filters' config - if ($vlv_page === null && !empty($this->prop['group_filters'])) { + if ($vlv_page === null && $search === null && is_array($this->prop['group_filters'])) { $groups = array(); $rcube = rcube::get_instance(); @@ -1644,7 +1669,7 @@ class rcube_ldap extends rcube_addressbook return $groups; } - if ($this->cache && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) { + if ($this->cache && $search === null && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) { return $groups; } @@ -1666,12 +1691,26 @@ class rcube_ldap extends rcube_addressbook } $ldap = clone $this->ldap; - $ldap->set_config($this->prop['groups']); + $ldap->config_set($this->prop['groups']); $ldap->set_vlv_page($vlv_page+1, $page_size); } - $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)); - $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $this->prop['groups']); + $props = array('sort' => $this->prop['groups']['sort']); + $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)); + + // add search filter + if ($search !== null) { + // set wildcards + $wp = $ws = ''; + if (!empty($this->prop['fuzzy_search']) && $mode != 1) { + $ws = '*'; + $wp = !$mode ? '*' : ''; + } + $filter = "(&$filter($name_attr=$wp" . rcube_ldap_generic::quote_string($search) . "$ws))"; + $props['search'] = $wp . $search . $ws; + } + + $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $props); if ($ldap_data === false) { return array(); @@ -1708,7 +1747,7 @@ class rcube_ldap extends rcube_addressbook // call recursively until we have fetched all groups while ($this->prop['groups']['vlv'] && $group_count == $page_size) { - $next_page = $this->_fetch_groups(++$vlv_page); + $next_page = $this->_fetch_groups($search, $mode, ++$vlv_page); $groups = array_merge($groups, $next_page); $group_count = count($next_page); } @@ -1719,9 +1758,12 @@ class rcube_ldap extends rcube_addressbook } // cache this - if ($this->cache) { + if ($this->cache && $search === null) { $this->cache->set('groups', $groups); } + else if ($search !== null) { + $this->group_search_cache = $groups; + } return $groups; } @@ -1790,7 +1832,7 @@ class rcube_ldap extends rcube_addressbook $member_attr => '', ); - if (!$this->ldap->add($new_dn, $new_entry)) { + if (!$this->ldap->add_entry($new_dn, $new_entry)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1813,7 +1855,7 @@ class rcube_ldap extends rcube_addressbook $group_cache = $this->_fetch_groups(); $del_dn = $group_cache[$group_id]['dn']; - if (!$this->ldap->delete($del_dn)) { + if (!$this->ldap->delete_entry($del_dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php index f1048ef39..a76ad6d06 100644 --- a/program/lib/Roundcube/rcube_ldap_generic.php +++ b/program/lib/Roundcube/rcube_ldap_generic.php @@ -5,8 +5,8 @@ | Roundcube/rcube_ldap_generic.php | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2013, The Roundcube Dev Team | - | Copyright (C) 2012-2013, Kolab Systems AG | + | Copyright (C) 2006-2014, The Roundcube Dev Team | + | Copyright (C) 2012-2014, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -21,459 +21,136 @@ +-----------------------------------------------------------------------+ */ - -/* - LDAP connection properties - -------------------------- - - $prop = array( - 'host' => '<ldap-server-address>', - // or - 'hosts' => array('directory.verisign.com'), - 'port' => 389, - 'use_tls' => true|false, - 'ldap_version' => 3, // using LDAPv3 - 'auth_method' => '', // SASL authentication method (for proxy auth), e.g. DIGEST-MD5 - 'attributes' => array('dn'), // List of attributes to read from the server - 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it) - 'config_root_dn' => 'cn=config', // Root DN to read config (e.g. vlv indexes) from - 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting - 'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit. - 'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit. - 'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x - 'referrals' => true|false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups - ); -*/ - /** * Model class to access an LDAP directories * * @package Framework * @subpackage LDAP */ -class rcube_ldap_generic +class rcube_ldap_generic extends Net_LDAP3 { - const UPDATE_MOD_ADD = 1; - const UPDATE_MOD_DELETE = 2; - const UPDATE_MOD_REPLACE = 4; - const UPDATE_MOD_FULL = 7; - - public $conn; - public $vlv_active = false; - /** private properties */ protected $cache = null; - protected $config = array(); protected $attributes = array('dn'); - protected $entries = null; - protected $result = null; - protected $debug = false; - protected $list_page = 1; - protected $page_size = 10; - protected $vlv_config = null; - + protected $error; - /** - * Object constructor - * - * @param array $p LDAP connection properties - */ - function __construct($p) + function __construct($config = null) { - $this->config = $p; + parent::__construct($config); - if (is_array($p['attributes'])) - $this->attributes = $p['attributes']; - - if (!is_array($p['hosts']) && !empty($p['host'])) - $this->config['hosts'] = array($p['host']); + $this->config_set('log_hook', array($this, 'log')); } /** - * Activate/deactivate debug mode - * - * @param boolean $dbg True if LDAP commands should be logged + * Establish a connection to the LDAP server */ - public function set_debug($dbg = true) + public function connect($host = null) { - $this->debug = $dbg; - } + // Net_LDAP3 does not support IDNA yet + // also parse_host() here is very Roundcube specific + $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); - /** - * Set connection options - * - * @param mixed $opt Option name as string or hash array with multiple options - * @param mixed $val Option value - */ - public function set_config($opt, $val = null) - { - if (is_array($opt)) - $this->config = array_merge($this->config, $opt); - else - $this->config[$opt] = $value; + return parent::connect($host); } /** - * Enable caching by passing an instance of rcube_cache to be used by this object + * Get a specific LDAP entry, identified by its DN * - * @param object rcube_cache Instance or False to disable caching - */ - public function set_cache($cache_engine) - { - $this->cache = $cache_engine; - } - - /** - * Set properties for VLV-based paging + * @param string $dn Record identifier * - * @param number $page Page number to list (starting at 1) - * @param number $size Number of entries to display on one page + * @return array Hash array */ - public function set_vlv_page($page, $size = 10) - { - $this->list_page = $page; - $this->page_size = $size; - } - - /** - * Establish a connection to the LDAP server - */ - public function connect($host = null) + function get_entry($dn) { - if (!function_exists('ldap_connect')) { - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "No ldap support in this installation of PHP"), - true); - return false; - } - - if (is_resource($this->conn) && $this->config['host'] == $host) - return true; - - if (empty($this->config['ldap_version'])) - $this->config['ldap_version'] = 3; - - // iterate over hosts if none specified - if (!$host) { - if (!is_array($this->config['hosts'])) - $this->config['hosts'] = array($this->config['hosts']); - - foreach ($this->config['hosts'] as $host) { - if (!empty($host) && $this->connect($host)) { - return true; - } - } - - return false; - } - - // open connection to the given $host - $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); - $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : ''); - - $this->_debug("C: Connect to $hostname [{$this->config['name']}]"); - - if ($lc = @ldap_connect($host, $this->config['port'])) { - if ($this->config['use_tls'] === true) - if (!ldap_start_tls($lc)) - continue; - - $this->_debug("S: OK"); - - ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']); - $this->config['host'] = $host; - $this->conn = $lc; - - if (!empty($this->config['network_timeout'])) - ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']); - - if (isset($this->config['referrals'])) - ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']); - } - else { - $this->_debug("S: NOT OK"); - } - - 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; - } - - return true; + return parent::get_entry($dn, $this->attributes); } /** - * 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 + * Prints debug/error info to the log */ - public function sasl_bind($authc, $pass, $authz=null) + public function log($level, $msg) { - if (!$this->conn) { - return false; - } + $msg = implode("\n", $msg); - if (!function_exists('ldap_sasl_bind')) { - rcube::raise_error(array('code' => 100, 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Unable to bind: ldap_sasl_bind() not exists"), - true); - return false; - } - - if (!empty($authz)) { - $authz = 'u:' . $authz; - } - - if (!empty($this->config['auth_method'])) { - $method = $this->config['auth_method']; - } - else { - $method = 'DIGEST-MD5'; - } + switch ($level) { + case LOG_DEBUG: + case LOG_INFO: + case LOG_NOTICE: + if ($this->config['debug']) { + rcube::write_log('ldap', $msg); + } + break; - $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: **** [" . strlen($pass) . "]"); + case LOG_EMERGE: + case LOG_ALERT: + case LOG_CRIT: + rcube::raise_error($msg, true, true); + break; - if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) { - $this->_debug("S: OK"); - return true; + case LOG_ERR: + case LOG_WARNING: + $this->error = $msg; + rcube::raise_error($msg, true, false); + break; } - - $this->_debug("S: ".ldap_error($this->conn)); - - rcube::raise_error(array( - 'code' => ldap_errno($this->conn), 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)), - true); - return false; } /** - * Bind connection with DN and password - * - * @param string $dn Bind DN - * @param string $pass Bind password + * Returns the last LDAP error occurred * - * @return boolean True on success, False on error - */ - public function bind($dn, $pass) - { - if (!$this->conn) { - return false; - } - - $this->_debug("C: Bind $dn, pass: **** [" . strlen($pass) . "]"); - - if (@ldap_bind($this->conn, $dn, $pass)) { - $this->_debug("S: OK"); - return true; - } - - $this->_debug("S: ".ldap_error($this->conn)); - - rcube::raise_error(array( - 'code' => ldap_errno($this->conn), 'type' => 'ldap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)), - true); - - return false; - } - - /** - * Close connection to LDAP server + * @return mixed Error message string or null if no error occured */ - public function close() + function get_error() { - if ($this->conn) { - $this->_debug("C: Close"); - ldap_unbind($this->conn); - $this->conn = null; - } + return $this->error; } /** - * Return the last result set - * - * @return object rcube_ldap_result Result object + * @deprecated */ - function get_result() + public function set_debug($dbg = true) { - return $this->result; + $this->config['debug'] = (bool) $dbg; } /** - * Get a specific LDAP entry, identified by its DN - * - * @param string $dn Record identifier - * @return array Hash array + * @deprecated */ - function get_entry($dn) + public function set_cache($cache_engine) { - $rec = null; - - if ($this->conn && $dn) { - $this->_debug("C: Read $dn [(objectclass=*)]"); - - if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) { - $this->_debug("S: OK"); - - if ($entry = ldap_first_entry($this->conn, $ldap_result)) { - $rec = ldap_get_attributes($this->conn, $entry); - } - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - - if (!empty($rec)) { - $rec['dn'] = $dn; // Add in the dn for the entry. - } - } - - return $rec; + $this->config['cache'] = $cache_engine; } /** - * Execute the LDAP search based on the stored credentials - * - * @param string $base_dn The base DN to query - * @param string $filter The LDAP filter for search - * @param string $scope The LDAP scope (list|sub|base) - * @param array $attrs List of entry attributes to read - * @param array $prop Hash array with query configuration properties: - * - sort: array of sort attributes (has to be in sync with the VLV index) - * - search: search string used for VLV controls - * @param boolean $count_only Set to true if only entry count is requested - * - * @return mixed rcube_ldap_result object or number of entries (if count_only=true) or false on error + * @deprecated */ - public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false) + public static function scope2func($scope, &$ns_function = null) { - if (!$this->conn) { - return false; - } - - if (empty($filter)) { - $filter = '(objectclass=*)'; - } - - $this->_debug("C: Search $base_dn for $filter"); - - $function = self::scope2func($scope, $ns_function); - - // find available VLV index for this query - if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) { - // when using VLV, we get the total count by... - // ...either reading numSubOrdinates attribute - if (($sub_filter = $this->config['numsub_filter']) && - ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0)) - ) { - $counts = ldap_get_entries($this->conn, $result_count); - for ($vlv_count = $j = 0; $j < $counts['count']; $j++) - $vlv_count += $counts[$j]['numsubordinates'][0]; - $this->_debug("D: total numsubordinates = " . $vlv_count); - } - // ...or by fetching all records dn and count them - else if (!function_exists('ldap_parse_virtuallist_control')) { - $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true); - } - - $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']); - } - else { - $this->vlv_active = false; - } - - // only fetch dn for count (should keep the payload low) - if ($ldap_result = @$function($this->conn, $base_dn, $filter, - $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']) - ) { - // when running on a patched PHP we can use the extended functions - // to retrieve the total count from the LDAP search result - if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) { - if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) { - ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult); - $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count"); - } - else { - $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn))); - } - } - else if ($this->debug) { - $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found"); - } - - $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count); - - return $count_only ? $this->result->count() : $this->result; - } - else { - $this->_debug("S: ".ldap_error($this->conn)); - } - - return false; + return self::scope_to_function($scope, $ns_function); } /** - * Modify an LDAP entry on the server - * - * @param string $dn Entry DN - * @param array $params Hash array of entry attributes - * @param int $mode Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE) + * @deprecated */ - public function modify($dn, $parms, $mode = 255) + public function set_config($opt, $val = null) { - // TODO: implement this - - return false; + $this->config_set($opt, $val); } /** - * Wrapper for ldap_add() - * - * @see ldap_add() + * @deprecated */ public function add($dn, $entry) { - $this->_debug("C: Add $dn: ".print_r($entry, true)); - - $res = ldap_add($this->conn, $dn, $entry); - if ($res === false) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; + return $this->add_entry($dn, $entry); } /** - * Wrapper for ldap_delete() - * - * @see ldap_delete() + * @deprecated */ public function delete($dn) { - $this->_debug("C: Delete $dn"); - - $res = ldap_delete($this->conn, $dn); - if ($res === false) { - $this->_debug("S: ".ldap_error($this->conn)); - return false; - } - - $this->_debug("S: OK"); - return true; + return $this->delete_entry($dn); } /** @@ -486,7 +163,7 @@ class rcube_ldap_generic $this->_debug("C: Replace $dn: ".print_r($entry, true)); if (!ldap_mod_replace($this->conn, $dn, $entry)) { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_mod_replace() failed with " . ldap_error($this->conn)); return false; } @@ -504,7 +181,7 @@ class rcube_ldap_generic $this->_debug("C: Add $dn: ".print_r($entry, true)); if (!ldap_mod_add($this->conn, $dn, $entry)) { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_mod_add() failed with " . ldap_error($this->conn)); return false; } @@ -522,7 +199,7 @@ class rcube_ldap_generic $this->_debug("C: Delete $dn: ".print_r($entry, true)); if (!ldap_mod_del($this->conn, $dn, $entry)) { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_mod_del() failed with " . ldap_error($this->conn)); return false; } @@ -540,7 +217,7 @@ class rcube_ldap_generic $this->_debug("C: Rename $dn to $newrdn"); if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_rename() failed with " . ldap_error($this->conn)); return false; } @@ -563,7 +240,7 @@ class rcube_ldap_generic $list = ldap_get_entries($this->conn, $result); if ($list === false) { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_get_entries() failed with " . ldap_error($this->conn)); return array(); } @@ -573,7 +250,7 @@ class rcube_ldap_generic $this->_debug("S: $count record(s)"); } else { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_list() failed with " . ldap_error($this->conn)); } return $list; @@ -590,12 +267,9 @@ class rcube_ldap_generic $this->_debug("C: Read $dn [{$filter}]"); if ($this->conn && $dn) { - if (!$attributes) - $attributes = $this->attributes; - $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']); if ($result === false) { - $this->_debug("S: ".ldap_error($this->conn)); + $this->_error("ldap_read() failed with " . ldap_error($this->conn)); return false; } @@ -607,97 +281,11 @@ class rcube_ldap_generic } /** - * Choose the right PHP function according to scope property - * - * @param string $scope The LDAP scope (sub|base|list) - * @param string $ns_function Function to be used for numSubOrdinates queries - * @return string PHP function to be used to query directory - */ - public static function scope2func($scope, &$ns_function = null) - { - switch ($scope) { - case 'sub': - $function = $ns_function = 'ldap_search'; - break; - case 'base': - $function = $ns_function = 'ldap_read'; - break; - default: - $function = 'ldap_list'; - $ns_function = 'ldap_read'; - break; - } - - return $function; - } - - /** - * Convert the given scope integer value to a string representation - */ - public static function scopeint2str($scope) - { - switch ($scope) { - case 2: return 'sub'; - case 1: return 'one'; - case 0: return 'base'; - default: $this->_debug("Scope $scope is not a valid scope integer"); - } - - return ''; - } - - /** - * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters. - * - * @param string $val Value to quote - * @return string The escaped value - */ - public static function escape_value($val) - { - return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', - '\\'=>'\5c', '/'=>'\2f')); - } - - /** - * Escapes a DN value according to RFC 2253 - * - * @param string $dn DN value o quote - * @return string The escaped value - */ - public static function escape_dn($dn) - { - return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b', - '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', - '"'=>'\22', '#'=>'\23')); - } - - /** - * Normalize a LDAP result by converting entry attributes arrays into single values - * - * @param array $result LDAP result set fetched with ldap_get_entries() - * @return array Hash array with normalized entries, indexed by their DNs - */ - public static function normalize_result($result) - { - if (!is_array($result)) { - return array(); - } - - $entries = array(); - for ($i = 0; $i < $result['count']; $i++) { - $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i; - $entries[$key] = self::normalize_entry($result[$i]); - } - - return $entries; - } - - /** * Turn an LDAP entry into a regular PHP array with attributes as keys. * * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries() * - * @return array Hash array with attributes as keys + * @return array Hash array with attributes as keys */ public static function normalize_entry($entry) { @@ -728,328 +316,7 @@ class rcube_ldap_generic return $rec; } - - /** - * Set server controls for Virtual List View (paginated listing) - */ - private function _vlv_set_controls($sort, $list_page, $page_size, $search = null) - { - $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => self::_sort_ber_encode((array)$sort)); - $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true); - - $this->_debug("C: Set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);" - . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)"); - - if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) { - $this->_debug("S: ".ldap_error($this->conn)); - $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported'); - return false; - } - - return true; - } - - /** - * Returns unified attribute name (resolving aliases) - */ - private static function _attr_name($namev) - { - // list of known attribute aliases - static $aliases = array( - 'gn' => 'givenname', - 'rfc822mailbox' => 'email', - 'userid' => 'uid', - 'emailaddress' => 'email', - 'pkcs9email' => 'email', - ); - - list($name, $limit) = explode(':', $namev, 2); - $suffix = $limit ? ':'.$limit : ''; - - return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix; - } - - /** - * Quotes attribute value string - * - * @param string $str Attribute value - * @param bool $dn True if the attribute is a DN - * - * @return string Quoted string - */ - public static function quote_string($str, $dn=false) - { - // take firt entry if array given - if (is_array($str)) - $str = reset($str); - - if ($dn) - $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c', - '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23'); - else - $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c', - '/'=>'\2f'); - - return strtr($str, $replace); - } - - /** - * Prints debug info to the log - */ - private function _debug($str) - { - if ($this->debug && class_exists('rcube')) { - rcube::write_log('ldap', $str); - } - } - - - /***************** Virtual List View (VLV) related utility functions **************** */ - - /** - * Return the search string value to be used in VLV controls - */ - private function _vlv_search($sort, $search) - { - foreach ($search as $attr => $value) { - if (!in_array(strtolower($attr), $sort)) { - $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")"); - return null; - } else { - return $value; - } - } - } - - /** - * Find a VLV index matching the given query attributes - * - * @return string Sort attribute or False if no match - */ - private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null) - { - if (!$this->config['vlv'] || $scope == 'base') { - return false; - } - - // get vlv config - $vlv_config = $this->_read_vlv_config(); - - if ($vlv = $vlv_config[$base_dn]) { - $this->_debug("D: Found a VLV for $base_dn"); - - if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) { - $this->_debug("D: Filter matches"); - if ($vlv['scope'] == $scope) { - // Not passing any sort attributes means you don't care - if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) { - return $vlv['sort'][0]; - } - } - else { - $this->_debug("D: Scope does not match"); - } - } - else { - $this->_debug("D: Filter does not match"); - } - } - else { - $this->_debug("D: No VLV for $base_dn"); - } - - return false; - } - - /** - * Return VLV indexes and searches including necessary configuration - * details. - */ - private function _read_vlv_config() - { - if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) { - return array(); - } - // return hard-coded VLV config - else if (is_array($this->config['vlv'])) { - return $this->config['vlv']; - } - - // return cached result - if (is_array($this->vlv_config)) { - return $this->vlv_config; - } - - if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) { - $this->vlv_config = $cached_config; - return $this->vlv_config; - } - - $this->vlv_config = array(); - - $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0); - $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)'); - - if ($vlv_searches->count() < 1) { - $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'"); - return array(); - } - - foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) { - // Multiple indexes may exist - $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0); - $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)'); - - // Reset this one for each VLV search. - $_vlv_sort = array(); - foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) { - $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']); - } - - $this->vlv_config[$vlv_search_attrs['vlvbase']] = array( - 'scope' => self::scopeint2str($vlv_search_attrs['vlvscope']), - 'filter' => strtolower($vlv_search_attrs['vlvfilter']), - 'sort' => $_vlv_sort, - ); - } - - // cache this - if ($this->cache) - $this->cache->set('vlvconfig', $this->vlv_config); - - $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true)); - - return $this->vlv_config; - } - - /** - * Generate BER encoded string for Virtual List View option - * - * @param integer List offset (first record) - * @param integer Records per page - * - * @return string BER encoded option value - */ - private static function _vlv_ber_encode($offset, $rpp, $search = '') - { - /* - this string is ber-encoded, php will prefix this value with: - 04 (octet string) and 10 (length of 16 bytes) - the code behind this string is broken down as follows: - 30 = ber sequence with a length of 0e (14) bytes following - 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0) - 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24) - a0 = type context-specific/constructed with a length of 06 (6) bytes following - 02 = type integer with 2 bytes following (offset): 01 01 (ie 1) - 02 = type integer with 2 bytes following (contentCount): 01 00 - - with a search string present: - 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here) - 81 indicates a user string is present where as a a0 indicates just a offset search - 81 = type context-specific/constructed with a length of 06 (6) bytes following - - The following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the - encoding of integer values (note: these values are in - two-complement form so since offset will never be negative bit 8 of the - leftmost octet should never by set to 1): - 8.3.2: If the contents octets of an integer value encoding consist - of more than one octet, then the bits of the first octet (rightmost) - and bit 8 of the second (to the left of first octet) octet: - a) shall not all be ones; and - b) shall not all be zero - */ - - if ($search) { - $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search); - $ber_val = self::_string2hex($search); - $str = self::_ber_addseq($ber_val, '81'); - } - else { - // construct the string from right to left - $str = "020100"; # contentCount - - $ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format - - // calculate octet length of $ber_val - $str = self::_ber_addseq($ber_val, '02') . $str; - - // now compute length over $str - $str = self::_ber_addseq($str, 'a0'); - } - - // now tack on records per page - $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str; - - // now tack on sequence identifier and length - $str = self::_ber_addseq($str, '30'); - - return pack('H'.strlen($str), $str); - } - - /** - * create ber encoding for sort control - * - * @param array List of cols to sort by - * @return string BER encoded option value - */ - private static function _sort_ber_encode($sortcols) - { - $str = ''; - foreach (array_reverse((array)$sortcols) as $col) { - $ber_val = self::_string2hex($col); - - // 30 = ber sequence with a length of octet value - // 04 = octet string with a length of the ascii value - $oct = self::_ber_addseq($ber_val, '04'); - $str = self::_ber_addseq($oct, '30') . $str; - } - - // now tack on sequence identifier and length - $str = self::_ber_addseq($str, '30'); - - return pack('H'.strlen($str), $str); - } - - /** - * Add BER sequence with correct length and the given identifier - */ - private static function _ber_addseq($str, $identifier) - { - $len = dechex(strlen($str)/2); - if (strlen($len) % 2 != 0) - $len = '0'.$len; - - return $identifier . $len . $str; - } - - /** - * Returns BER encoded integer value in hex format - */ - private static function _ber_encode_int($offset) - { - $val = dechex($offset); - $prefix = ''; - - // check if bit 8 of high byte is 1 - if (preg_match('/^[89abcdef]/', $val)) - $prefix = '00'; - - if (strlen($val)%2 != 0) - $prefix .= '0'; - - return $prefix . $val; - } - - /** - * Returns ascii string encoded in hex - */ - private static function _string2hex($str) - { - $hex = ''; - for ($i=0; $i < strlen($str); $i++) { - $hex .= dechex(ord($str[$i])); - } - return $hex; - } - } + +// for backward compat. +class rcube_ldap_result extends Net_LDAP3_Result {} diff --git a/program/lib/Roundcube/rcube_ldap_result.php b/program/lib/Roundcube/rcube_ldap_result.php deleted file mode 100644 index efc3331bc..000000000 --- a/program/lib/Roundcube/rcube_ldap_result.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php - -/* - +-----------------------------------------------------------------------+ - | Roundcube/rcube_ldap_result.php | - | | - | This file is part of the Roundcube Webmail client | - | Copyright (C) 2006-2013, The Roundcube Dev Team | - | Copyright (C) 2013, Kolab Systems AG | - | | - | Licensed under the GNU General Public License version 3 or | - | any later version with exceptions for skins & plugins. | - | See the README file for a full license statement. | - | | - | PURPOSE: | - | Model class that represents an LDAP search result | - | | - +-----------------------------------------------------------------------+ - | Author: Thomas Bruederli <roundcube@gmail.com> | - +-----------------------------------------------------------------------+ -*/ - - -/** - * Model class representing an LDAP search result - * - * @package Framework - * @subpackage LDAP - */ -class rcube_ldap_result implements Iterator -{ - public $conn; - public $ldap; - public $base_dn; - public $filter; - - private $count = null; - private $current = null; - private $iteratorkey = 0; - - /** - * Default constructor - * - * @param resource $conn LDAP link identifier - * @param resource $ldap LDAP result entry identifier - * @param string $base_dn Base DN used to get this result - * @param string $filter Filter query used to get this result - * @param integer $count Record count value (pre-calculated) - */ - function __construct($conn, $ldap, $base_dn, $filter, $count = null) - { - $this->conn = $conn; - $this->ldap = $ldap; - $this->base_dn = $base_dn; - $this->filter = $filter; - $this->count = $count; - } - - /** - * Wrapper for ldap_sort() - */ - public function sort($attr) - { - return ldap_sort($this->conn, $this->ldap, $attr); - } - - /** - * Get entries count - */ - public function count() - { - if (!isset($this->count)) - $this->count = ldap_count_entries($this->conn, $this->ldap); - - return $this->count; - } - - /** - * Wrapper for ldap_get_entries() - * - * @param boolean $normalize Optionally normalize the entries to a list of hash arrays - * @return array List of LDAP entries - */ - public function entries($normalize = false) - { - $entries = ldap_get_entries($this->conn, $this->ldap); - return $normalize ? rcube_ldap_generic::normalize_result($entries) : $entries; - } - - /** - * Wrapper for ldap_get_dn() using the current entry pointer - */ - public function get_dn() - { - return $this->current ? ldap_get_dn($this->conn, $this->current) : null; - } - - - /*** Implements the PHP 5 Iterator interface to make foreach work ***/ - - function current() - { - $attrib = ldap_get_attributes($this->conn, $this->current); - $attrib['dn'] = ldap_get_dn($this->conn, $this->current); - return $attrib; - } - - function key() - { - return $this->iteratorkey; - } - - function rewind() - { - $this->iteratorkey = 0; - $this->current = ldap_first_entry($this->conn, $this->ldap); - } - - function next() - { - $this->iteratorkey++; - $this->current = ldap_next_entry($this->conn, $this->current); - } - - function valid() - { - return (bool)$this->current; - } - -} diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php index 2bda930eb..2b795e591 100644 --- a/program/lib/Roundcube/rcube_message_header.php +++ b/program/lib/Roundcube/rcube_message_header.php @@ -225,8 +225,16 @@ class rcube_message_header } if ($decode) { - $value = rcube_mime::decode_header($value, $this->charset); - $value = rcube_charset::clean($value); + if (is_array($value)) { + foreach ($value as $key => $val) { + $value[$key] = rcube_mime::decode_header($val, $this->charset); + $value[$key] = rcube_charset::clean($val); + } + } + else { + $value = rcube_mime::decode_header($value, $this->charset); + $value = rcube_charset::clean($value); + } } return $value; diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index 091b2fae8..f66cf1437 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -394,6 +394,7 @@ class rcube_mime } if ($address) { + $address = self::fix_email($address); $result[$key] = array('name' => $name, 'address' => $address); } } @@ -480,15 +481,17 @@ class rcube_mime /** * Interpret a format=flowed message body according to RFC 2646 * - * @param string $text Raw body formatted as flowed text + * @param string $text Raw body formatted as flowed text + * @param string $mark Mark each flowed line with specified character * * @return string Interpreted text with unwrapped lines and stuffed space removed */ - public static function unfold_flowed($text) + public static function unfold_flowed($text, $mark = null) { $text = preg_split('/\r?\n/', $text); $last = -1; $q_level = 0; + $marks = array(); foreach ($text as $idx => $line) { if (preg_match('/^(>+)/', $line, $m)) { @@ -508,6 +511,10 @@ class rcube_mime ) { $text[$last] .= $line; unset($text[$idx]); + + if ($mark) { + $marks[$last] = true; + } } else { $last = $idx; @@ -520,7 +527,7 @@ class rcube_mime } else { // remove space-stuffing - $line = preg_replace('/^\s/', '', $line); + $line = preg_replace('/^ /', '', $line); if (isset($text[$last]) && $line && $text[$last] != '-- ' @@ -528,6 +535,10 @@ class rcube_mime ) { $text[$last] .= $line; unset($text[$idx]); + + if ($mark) { + $marks[$last] = true; + } } else { $text[$idx] = $line; @@ -538,6 +549,12 @@ class rcube_mime $q_level = $q; } + if (!empty($marks)) { + foreach (array_keys($marks) as $mk) { + $text[$mk] = $mark . $text[$mk]; + } + } + return implode("\r\n", $text); } @@ -806,6 +823,7 @@ class rcube_mime $file_paths[] = '/etc/httpd2/mime.types'; $file_paths[] = '/etc/apache/mime.types'; $file_paths[] = '/etc/apache2/mime.types'; + $file_paths[] = '/etc/nginx/mime.types'; $file_paths[] = '/usr/local/etc/httpd/conf/mime.types'; $file_paths[] = '/usr/local/etc/apache/conf/mime.types'; } @@ -889,4 +907,19 @@ class rcube_mime return 'image/' . $type; } + /** + * Try to fix invalid email addresses + */ + public static function fix_email($email) + { + $parts = rcube_utils::explode_quoted_string('@', $email); + foreach ($parts as $idx => $part) { + // remove redundant quoting (#1490040) + if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) { + $parts[$idx] = $m[1]; + } + } + + return implode('@', $parts); + } } diff --git a/program/lib/Roundcube/rcube_output.php b/program/lib/Roundcube/rcube_output.php index 1907645b0..55a38b240 100644 --- a/program/lib/Roundcube/rcube_output.php +++ b/program/lib/Roundcube/rcube_output.php @@ -44,7 +44,6 @@ abstract class rcube_output $this->browser = new rcube_browser(); } - /** * Magic getter */ @@ -60,7 +59,6 @@ abstract class rcube_output return null; } - /** * Setter for output charset. * To be specified in a meta tag and sent as http-header @@ -72,7 +70,6 @@ abstract class rcube_output $this->charset = $charset; } - /** * Getter for output charset * @@ -83,7 +80,6 @@ abstract class rcube_output return $this->charset; } - /** * Set environment variable * @@ -95,7 +91,6 @@ abstract class rcube_output $this->env[$name] = $value; } - /** * Environment variable getter. * @@ -108,7 +103,6 @@ abstract class rcube_output return $this->env[$name]; } - /** * Delete all stored env variables and commands */ @@ -117,7 +111,6 @@ abstract class rcube_output $this->env = array(); } - /** * Invoke display_message command * @@ -129,7 +122,6 @@ abstract class rcube_output */ abstract function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0); - /** * Redirect to a certain url. * @@ -138,13 +130,11 @@ abstract class rcube_output */ abstract function redirect($p = array(), $delay = 1); - /** * Send output to the client. */ abstract function send(); - /** * Send HTTP headers to prevent caching a page */ @@ -157,9 +147,6 @@ abstract class rcube_output 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'); @@ -178,14 +165,32 @@ abstract class rcube_output */ public function future_expire_header($offset = 2600000) { - if (headers_sent()) + if (headers_sent()) { return; + } header("Expires: " . gmdate("D, d M Y H:i:s", time()+$offset) . " GMT"); header("Cache-Control: max-age=$offset"); header("Pragma: "); } + /** + * Send browser compatibility/security/etc. headers + */ + public function common_headers() + { + if (headers_sent()) { + return; + } + + // Unlock IE compatibility mode + if ($this->browser->ie) { + header('X-UA-Compatible: IE=edge'); + } + + // Request browser to disable DNS prefetching (CVE-2010-0464) + header("X-DNS-Prefetch-Control: off"); + } /** * Show error page and terminate script execution @@ -200,7 +205,6 @@ abstract class rcube_output exit(-1); } - /** * Create an edit field for inclusion on a form * @@ -253,7 +257,6 @@ abstract class rcube_output return $out; } - /** * Convert a variable into a javascript object notation * @@ -269,5 +272,4 @@ abstract class rcube_output // that's why we have @ here return @json_encode($input); } - } diff --git a/program/lib/Roundcube/rcube_plugin.php b/program/lib/Roundcube/rcube_plugin.php index 01c340deb..9c7a9c2dc 100644 --- a/program/lib/Roundcube/rcube_plugin.php +++ b/program/lib/Roundcube/rcube_plugin.php @@ -94,7 +94,15 @@ abstract class rcube_plugin /** * Provide information about this * - * @return array Meta information about a plugin or false if not implemented + * @return array Meta information about a plugin or false if not implemented: + * As hash array with the following keys: + * name: The plugin name + * vendor: Name of the plugin developer + * version: Plugin version name + * license: License name (short form according to http://spdx.org/licenses/) + * uri: The URL to the plugin homepage or source repository + * src_uri: Direct download URL to the source code of this plugin + * require: List of plugins required for this one (as array of plugin names) */ public static function info() { @@ -113,6 +121,17 @@ abstract class rcube_plugin } /** + * Attempt to load the given plugin which is optional for the current plugin + * + * @param string Plugin name + * @return boolean True on success, false on failure + */ + public function include_plugin($plugin_name) + { + return $this->api->load_plugin($plugin_name, true, false); + } + + /** * Load local config file from plugins directory. * The loaded values are patched over the global configuration. * diff --git a/program/lib/Roundcube/rcube_plugin_api.php b/program/lib/Roundcube/rcube_plugin_api.php index feeeb192e..e0b8aea38 100644 --- a/program/lib/Roundcube/rcube_plugin_api.php +++ b/program/lib/Roundcube/rcube_plugin_api.php @@ -38,6 +38,7 @@ class rcube_plugin_api public $handlers = array(); public $allowed_prefs = array(); public $allowed_session_prefs = array(); + public $active_plugins = array(); protected $plugins = array(); protected $tasks = array(); @@ -45,7 +46,7 @@ class rcube_plugin_api protected $actionmap = array(); protected $objectsmap = array(); protected $template_contents = array(); - protected $active_hook = false; + protected $exec_stack = array(); // Deprecated names of hooks, will be removed after 0.5-stable release protected $deprecated_hooks = array( @@ -169,10 +170,11 @@ class rcube_plugin_api * * @param string Plugin name * @param boolean Force loading of the plugin even if it doesn't match the filter + * @param boolean Require loading of the plugin, error if it doesn't exist * * @return boolean True on success, false if not loaded or failure */ - public function load_plugin($plugin_name, $force = false) + public function load_plugin($plugin_name, $force = false, $require = true) { static $plugins_dir; @@ -186,10 +188,9 @@ class rcube_plugin_api return true; } - $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name - . DIRECTORY_SEPARATOR . $plugin_name . '.php'; + $fn = "$plugins_dir/$plugin_name/$plugin_name.php"; - if (file_exists($fn)) { + if (is_readable($fn)) { if (!class_exists($plugin_name, false)) { include $fn; } @@ -197,6 +198,8 @@ class rcube_plugin_api // instantiate class if exists if (class_exists($plugin_name, false)) { $plugin = new $plugin_name($this); + $this->active_plugins[] = $plugin_name; + // check inheritance... if (is_subclass_of($plugin, 'rcube_plugin')) { // ... task, request type and framed mode @@ -222,7 +225,7 @@ class rcube_plugin_api true, false); } } - else { + elseif ($require) { rcube::raise_error(array('code' => 520, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to load plugin file $fn"), true, false); @@ -252,6 +255,9 @@ class rcube_plugin_api 'GPL-3.0' => 'http://www.gnu.org/licenses/gpl-3.0.html', 'GPL-3.0+' => 'http://www.gnu.org/licenses/gpl.html', 'GPL-2.0+' => 'http://www.gnu.org/licenses/gpl.html', + 'AGPLv3' => 'http://www.gnu.org/licenses/agpl.html', + 'AGPLv3+' => 'http://www.gnu.org/licenses/agpl.html', + 'AGPL-3.0' => 'http://www.gnu.org/licenses/agpl.html', 'LGPL' => 'http://www.gnu.org/licenses/lgpl.html', 'LGPLv2' => 'http://www.gnu.org/licenses/lgpl-2.0.html', 'LGPLv2.1' => 'http://www.gnu.org/licenses/lgpl-2.1.html', @@ -272,11 +278,15 @@ class rcube_plugin_api ); $dir = dir($this->dir); - $fn = unslashify($dir->path) . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php'; + $fn = unslashify($dir->path) . "/$plugin_name/$plugin_name.php"; $info = false; - if (!class_exists($plugin_name)) - include($fn); + if (!class_exists($plugin_name, false)) { + if (is_readable($fn)) + include($fn); + else + return false; + } if (class_exists($plugin_name)) $info = $plugin_name::info(); @@ -284,12 +294,17 @@ class rcube_plugin_api // fall back to composer.json file if (!$info) { $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json"; - if (file_exists($composer) && ($json = @json_decode(file_get_contents($composer), true))) { + if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) { list($info['vendor'], $info['name']) = explode('/', $json['name']); $info['version'] = $json['version']; $info['license'] = $json['license']; - if ($license_uri = $license_uris[$info['license']]) - $info['license_uri'] = $license_uri; + $info['uri'] = $json['homepage']; + $info['require'] = array_filter(array_keys((array)$json['require']), function($pname) { + if (strpos($pname, '/') == false) + return false; + list($vendor, $name) = explode('/', $pname); + return !($name == 'plugin-installer' || $vendor == 'pear-pear'); + }); } // read local composer.lock file (once) @@ -313,7 +328,7 @@ class rcube_plugin_api // fall back to package.xml file if (!$info) { $package = INSTALL_PATH . "/plugins/$plugin_name/package.xml"; - if (file_exists($package) && ($file = file_get_contents($package))) { + if (is_readable($package) && ($file = file_get_contents($package))) { $doc = new DOMDocument(); $doc->loadXML($file); $xpath = new DOMXPath($doc); @@ -337,11 +352,19 @@ class rcube_plugin_api $deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name'); for ($i = 0; $i < $deps->length; $i++) { $dn = $deps->item($i)->nodeValue; - $info['requires'][] = $dn; + $info['require'][] = $dn; } } } + // At least provide the name + if (!$info && class_exists($plugin_name)) { + $info = array('name' => $plugin_name, 'version' => '--'); + } + else if ($info['license'] && empty($info['license_uri']) && ($license_uri = $license_uris[$info['license']])) { + $info['license_uri'] = $license_uri; + } + return $info; } @@ -399,8 +422,10 @@ class rcube_plugin_api $args = array('arg' => $args); } + // TODO: avoid recusion by checking in_array($hook, $this->exec_stack) ? + $args += array('abort' => false); - $this->active_hook = $hook; + array_push($this->exec_stack, $hook); foreach ((array)$this->handlers[$hook] as $callback) { $ret = call_user_func($callback, $args); @@ -413,7 +438,7 @@ class rcube_plugin_api } } - $this->active_hook = false; + array_pop($this->exec_stack); return $args; } @@ -549,7 +574,7 @@ class rcube_plugin_api */ public function is_processing($hook = null) { - return $this->active_hook && (!$hook || $this->active_hook == $hook); + return count($this->exec_stack) > 0 && (!$hook || in_array($hook, $this->exec_stack)); } /** diff --git a/program/lib/Roundcube/rcube_result_multifolder.php b/program/lib/Roundcube/rcube_result_multifolder.php index 4bbd2188d..786ee85f6 100644 --- a/program/lib/Roundcube/rcube_result_multifolder.php +++ b/program/lib/Roundcube/rcube_result_multifolder.php @@ -26,16 +26,16 @@ */ class rcube_result_multifolder { - public $multi = true; - public $sets = array(); + public $multi = true; + public $sets = array(); public $incomplete = false; public $folder; - protected $meta = array(); - protected $index = array(); + protected $meta = array(); + protected $index = array(); protected $folders = array(); + protected $order = 'ASC'; protected $sorting; - protected $order = 'ASC'; /** @@ -44,7 +44,7 @@ class rcube_result_multifolder public function __construct($folders = array()) { $this->folders = $folders; - $this->meta = array('count' => 0); + $this->meta = array('count' => 0); } @@ -74,7 +74,8 @@ class rcube_result_multifolder // append UIDs to global index $folder = $result->get_parameters('MAILBOX'); - $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get()); + $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get()); + $this->index = array_merge($this->index, $index); } @@ -89,7 +90,7 @@ class rcube_result_multifolder } $this->sorting = $sort_field; - $this->order = $sort_order; + $this->order = $sort_order; } /** @@ -150,8 +151,10 @@ class rcube_result_multifolder if ($this->order != $set->get_parameters('ORDER')) { $set->revert(); } + $folder = $set->get_parameters('MAILBOX'); - $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $set->get()); + $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $set->get()); + $this->index = array_merge($this->index, $index); } } @@ -171,6 +174,7 @@ class rcube_result_multifolder if (!empty($this->folder)) { $msgid .= '-' . $this->folder; } + return array_search($msgid, $this->index); } @@ -188,6 +192,7 @@ class rcube_result_multifolder if ($set->get_parameters('MAILBOX') == $folder) { $set->filter($ids); } + $this->meta['count'] += $set->count(); } } @@ -267,8 +272,8 @@ class rcube_result_multifolder public function get_parameters($param=null) { $params = array( - 'SORT' => $this->sorting, - 'ORDER' => $this->order, + 'SORT' => $this->sorting, + 'ORDER' => $this->order, 'MAILBOX' => $this->folders, ); diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php index caca262c6..26f78433a 100644 --- a/program/lib/Roundcube/rcube_session.php +++ b/program/lib/Roundcube/rcube_session.php @@ -3,7 +3,7 @@ /* +-----------------------------------------------------------------------+ | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2012, The Roundcube Dev Team | + | Copyright (C) 2005-2014, The Roundcube Dev Team | | Copyright (C) 2011, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | @@ -47,6 +47,13 @@ class rcube_session private $storage; private $memcache; + /** + * Blocks session data from being written to database. + * Can be used if write-race conditions are to be expected + * @var boolean + */ + public $nowrite = false; + /** * Default constructor @@ -201,6 +208,9 @@ class rcube_session $table = $this->db->table_name('session'); $ts = microtime(true); + if ($this->nowrite) + return true; + // no session row in DB (db_read() returns false) if (!$this->key) { $oldvars = null; diff --git a/program/lib/Roundcube/rcube_spellchecker.php b/program/lib/Roundcube/rcube_spellchecker.php index e9a36072d..43bab08c4 100644 --- a/program/lib/Roundcube/rcube_spellchecker.php +++ b/program/lib/Roundcube/rcube_spellchecker.php @@ -226,7 +226,18 @@ class rcube_spellchecker else { $word = mb_substr($this->content, $item[1], $item[2], RCUBE_CHARSET); } - $result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4]; + + if (is_array($item[4])) { + $suggestions = $item[4]; + } + else if (empty($item[4])) { + $suggestions = array(); + } + else { + $suggestions = explode("\t", $item[4]); + } + + $result[$word] = $suggestions; } return $result; diff --git a/program/lib/Roundcube/rcube_storage.php b/program/lib/Roundcube/rcube_storage.php index 69d6d2fae..ccb28c680 100644 --- a/program/lib/Roundcube/rcube_storage.php +++ b/program/lib/Roundcube/rcube_storage.php @@ -152,6 +152,19 @@ abstract class rcube_storage /** + * Get connection/class option + * + * @param string $name Option name + * + * @param mixed Option value + */ + public function get_option($name) + { + return $this->options[$name]; + } + + + /** * Activate/deactivate debug mode. * * @param boolean $dbg True if conversation with the server should be logged @@ -905,9 +918,11 @@ abstract class rcube_storage /** * Get mailbox quota information. * + * @param string $folder Folder name + * * @return mixed Quota info or False if not supported */ - abstract function get_quota(); + abstract function get_quota($folder = null); /* ----------------------------------------- diff --git a/program/lib/Roundcube/rcube_string_replacer.php b/program/lib/Roundcube/rcube_string_replacer.php index 77b91d18b..ce61e5367 100644 --- a/program/lib/Roundcube/rcube_string_replacer.php +++ b/program/lib/Roundcube/rcube_string_replacer.php @@ -42,7 +42,7 @@ class rcube_string_replacer // 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%=#$@+?|!&\\/_~\\[\\]\\(\\){}\*-'; + $url2 = 'a-zA-Z0-9%=#$@+?|!&\\/_~\\[\\]\\(\\){}\*\x80-\xFE-'; $this->link_pattern = "/([\w]+:\/\/|\W[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)($utf_domain([$url1]*[$url2]+)*)/"; $this->mailto_pattern = "/(" diff --git a/program/lib/Roundcube/rcube_text2html.php b/program/lib/Roundcube/rcube_text2html.php new file mode 100644 index 000000000..0afc6d110 --- /dev/null +++ b/program/lib/Roundcube/rcube_text2html.php @@ -0,0 +1,309 @@ +<?php + +/** + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2008-2014, The Roundcube Dev Team | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Converts plain text to HTML | + +-----------------------------------------------------------------------+ + | Author: Aleksander Machniak <alec@alec.pl> | + +-----------------------------------------------------------------------+ + */ + +/** + * Converts plain text to HTML + * + * @package Framework + * @subpackage Utils + */ +class rcube_text2html +{ + /** + * Contains the HTML content after conversion. + * + * @var string $html + */ + protected $html; + + /** + * Contains the plain text. + * + * @var string $text + */ + protected $text; + + /** + * Configuration + * + * @var array $config + */ + protected $config = array( + // non-breaking space + 'space' => "\xC2\xA0", + // enables format=flowed parser + 'flowed' => false, + // enables wrapping for non-flowed text + 'wrap' => true, + // line-break tag + 'break' => "<br>\n", + // prefix and suffix (wrapper element) + 'begin' => '<div class="pre">', + 'end' => '</div>', + // enables links replacement + 'links' => true, + // string replacer class + 'replacer' => 'rcube_string_replacer', + ); + + + /** + * Constructor. + * + * If the plain text source string (or file) is supplied, the class + * will instantiate with that source propagated, all that has + * to be done it to call get_html(). + * + * @param string $source Plain text + * @param boolean $from_file Indicates $source is a file to pull content from + * @param array $config Class configuration + */ + function __construct($source = '', $from_file = false, $config = array()) + { + if (!empty($source)) { + $this->set_text($source, $from_file); + } + + if (!empty($config) && is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * Loads source text into memory, either from $source string or a file. + * + * @param string $source Plain text + * @param boolean $from_file Indicates $source is a file to pull content from + */ + function set_text($source, $from_file = false) + { + if ($from_file && file_exists($source)) { + $this->text = file_get_contents($source); + } + else { + $this->text = $source; + } + + $this->_converted = false; + } + + /** + * Returns the HTML content. + * + * @return string HTML content + */ + function get_html() + { + if (!$this->_converted) { + $this->_convert(); + } + + return $this->html; + } + + /** + * Prints the HTML. + */ + function print_html() + { + print $this->get_html(); + } + + /** + * Workhorse function that does actual conversion (calls _converter() method). + */ + protected function _convert() + { + // Convert TXT to HTML + $this->html = $this->_converter($this->text); + $this->_converted = true; + } + + /** + * Workhorse function that does actual conversion. + * + * @param string Plain text + */ + protected function _converter($text) + { + // make links and email-addresses clickable + $attribs = array('link_attribs' => array('rel' => 'noreferrer', 'target' => '_blank')); + $replacer = new $this->config['replacer']($attribs); + + if ($this->config['flowed']) { + $flowed_char = 0x01; + $text = rcube_mime::unfold_flowed($text, chr($flowed_char)); + } + + // search for patterns like links and e-mail addresses and replace with tokens + if ($this->config['links']) { + $text = $replacer->replace($text); + } + + // split body into single lines + $text = preg_split('/\r?\n/', $text); + $quote_level = 0; + $last = null; + + // wrap quoted lines with <blockquote> + for ($n = 0, $cnt = count($text); $n < $cnt; $n++) { + $flowed = false; + if ($this->config['flowed'] && ord($text[$n][0]) == $flowed_char) { + $flowed = true; + $text[$n] = substr($text[$n], 1); + } + + if ($text[$n][0] == '>' && preg_match('/^(>+ {0,1})+/', $text[$n], $regs)) { + $q = substr_count($regs[0], '>'); + $text[$n] = substr($text[$n], strlen($regs[0])); + $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']); + $_length = strlen(str_replace(' ', '', $text[$n])); + + if ($q > $quote_level) { + if ($last !== null) { + $text[$last] .= (!$length ? "\n" : '') + . $replacer->get_replacement($replacer->add( + str_repeat('<blockquote>', $q - $quote_level))) + . $text[$n]; + + unset($text[$n]); + } + else { + $text[$n] = $replacer->get_replacement($replacer->add( + str_repeat('<blockquote>', $q - $quote_level))) . $text[$n]; + + $last = $n; + } + } + else if ($q < $quote_level) { + $text[$last] .= (!$length ? "\n" : '') + . $replacer->get_replacement($replacer->add( + str_repeat('</blockquote>', $quote_level - $q))) + . $text[$n]; + + unset($text[$n]); + } + else { + $last = $n; + } + } + else { + $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']); + $q = 0; + $_length = strlen(str_replace(' ', '', $text[$n])); + + if ($quote_level > 0) { + $text[$last] .= (!$length ? "\n" : '') + . $replacer->get_replacement($replacer->add( + str_repeat('</blockquote>', $quote_level))) + . $text[$n]; + + unset($text[$n]); + } + else { + $last = $n; + } + } + + $quote_level = $q; + $length = $_length; + } + + if ($quote_level > 0) { + $text[$last] .= $replacer->get_replacement($replacer->add( + str_repeat('</blockquote>', $quote_level))); + } + + $text = join("\n", $text); + + // colorize signature (up to <sig_max_lines> lines) + $len = strlen($text); + $sig_sep = "--" . $this->config['space'] . "\n"; + $sig_max_lines = rcube::get_instance()->config->get('sig_max_lines', 15); + + while (($sp = strrpos($text, $sig_sep, $sp ? -$len+$sp-1 : 0)) !== false) { + if ($sp == 0 || $text[$sp-1] == "\n") { + // do not touch blocks with more that X lines + if (substr_count($text, "\n", $sp) < $sig_max_lines) { + $text = substr($text, 0, max(0, $sp)) + .'<span class="sig">'.substr($text, $sp).'</span>'; + } + + break; + } + } + + // insert url/mailto links and citation tags + $text = $replacer->resolve($text); + + // replace line breaks + $text = str_replace("\n", $this->config['break'], $text); + + return $this->config['begin'] . $text . $this->config['end']; + } + + /** + * Converts spaces in line of text + */ + protected function _convert_line($text, $is_flowed) + { + static $table; + + if (empty($table)) { + $table = get_html_translation_table(HTML_SPECIALCHARS); + unset($table['?']); + } + + // skip signature separator + if ($text == '-- ') { + return '--' . $this->config['space']; + } + + // replace HTML special characters + $text = strtr($text, $table); + + $nbsp = $this->config['space']; + + // replace some whitespace characters + $text = str_replace(array("\r", "\t"), array('', ' '), $text); + + // replace spaces with non-breaking spaces + if ($is_flowed) { + $pos = 0; + $diff = 0; + $len = strlen($nbsp); + $copy = $text; + + while (($pos = strpos($text, ' ', $pos)) !== false) { + if ($pos == 0 || $text[$pos-1] == ' ') { + $copy = substr_replace($copy, $nbsp, $pos + $diff, 1); + $diff += $len - 1; + } + $pos++; + } + + $text = $copy; + } + else { + // make the whole line non-breakable + $text = str_replace(array(' ', '-', '/'), array($nbsp, '-⁠', '/⁠'), $text); + } + + return $text; + } +} diff --git a/program/lib/Roundcube/rcube_utils.php b/program/lib/Roundcube/rcube_utils.php index c2009cee0..330322f10 100644 --- a/program/lib/Roundcube/rcube_utils.php +++ b/program/lib/Roundcube/rcube_utils.php @@ -119,17 +119,6 @@ class rcube_utils return true; } - if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) { - $lookup = array(); - @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup); - foreach ($lookup as $line) { - if (strpos($line, 'MX preference')) { - return true; - } - } - return false; - } - // find MX record(s) if (!function_exists('getmxrr') || getmxrr($domain_part, $mx_records)) { return true; @@ -795,7 +784,7 @@ class rcube_utils * * @return object DateTime instance or false on failure */ - public static function anytodatetime($date) + public static function anytodatetime($date, $timezone = null) { if (is_object($date) && is_a($date, 'DateTime')) { return $date; @@ -807,7 +796,7 @@ class rcube_utils // try to parse string with DateTime first if (!empty($date)) { try { - $dt = new DateTime($date); + $dt = new DateTime($date, $timezone); } catch (Exception $e) { // ignore @@ -928,7 +917,7 @@ class rcube_utils /** * Normalize the given string for fulltext search. - * Currently only optimized for Latin-1 characters; to be extended + * Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended * * @param string Input string (UTF-8) * @param boolean True to return list of words as array @@ -949,15 +938,32 @@ class rcube_utils // split by words $arr = self::tokenize_string($str); + // detect character set + if (utf8_encode(utf8_decode($str)) == $str) { + // ISO-8859-1 (or ASCII) + preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys); + preg_match_all('/./', 'aaaaaaaceeeeiiiinoooooouuuuyy', $values); + + $mapping = array_combine($keys[0], $values[0]); + $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')); + } + else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) { + // ISO-8859-2 + preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys); + preg_match_all('/./', 'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values); + + $mapping = array_combine($keys[0], $values[0]); + $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')); + } + foreach ($arr as $i => $part) { - 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'))); + $part = mb_strtolower($part); + + if (!empty($mapping)) { + $part = strtr($part, $mapping); } - else - $arr[$i] = mb_strtolower($part); + + $arr[$i] = $part; } return $as_array ? $arr : join(" ", $arr); @@ -1039,7 +1045,6 @@ class rcube_utils } } - /** * Find out if the string content means true or false * @@ -1063,7 +1068,37 @@ class rcube_utils return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path); } else { - return $path[0] == DIRECTORY_SEPARATOR; + return $path[0] == '/'; } } + + /** + * Resolve relative URL + * + * @param string $url Relative URL + * + * @return string Absolute URL + */ + public static function resolve_url($url) + { + // prepend protocol://hostname:port + if (!preg_match('|^https?://|', $url)) { + $schema = 'http'; + $default_port = 80; + + if (self::https_check()) { + $schema = 'https'; + $default_port = 443; + } + + $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']); + if ($_SERVER['SERVER_PORT'] != $default_port) { + $prefix .= ':' . $_SERVER['SERVER_PORT']; + } + + $url = $prefix . ($url[0] == '/' ? '' : '/') . $url; + } + + return $url; + } } diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php index fb8fdd525..96add110f 100644 --- a/program/lib/Roundcube/rcube_vcard.php +++ b/program/lib/Roundcube/rcube_vcard.php @@ -110,7 +110,7 @@ class rcube_vcard public function load($vcard, $charset = RCUBE_CHARSET, $detect = false) { self::$values_decoded = false; - $this->raw = self::vcard_decode($vcard); + $this->raw = self::vcard_decode(self::cleanup($vcard)); // resolve charset parameters if ($charset == null) { @@ -496,7 +496,7 @@ class rcube_vcard if (preg_match('/^END:VCARD$/i', $line)) { // parse vcard - $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap); + $obj = new rcube_vcard($vcard_block, $charset, true, self::$fieldmap); // FN and N is required by vCard format (RFC 2426) // on import we can be less restrictive, let's addressbook decide if (!empty($obj->displayname) || !empty($obj->surname) || !empty($obj->firstname) || !empty($obj->email)) { @@ -532,9 +532,9 @@ class rcube_vcard // Cleanup $vcard = preg_replace(array( // convert special types (like Skype) to normal type='skype' classes with this simple regex ;) - '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s', - '/^item\d*\.X-AB.*$/m', // remove cruft like item1.X-AB* - '/^item\d*\./m', // remove item1.ADR instead of ADR + '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./si', + '/^item\d*\.X-AB.*$/mi', // remove cruft like item1.X-AB* + '/^item\d*\./mi', // remove item1.ADR instead of ADR '/\n+/', // remove empty lines '/^(N:[^;\R]*)$/m', // if N doesn't have any semicolons, add some ), @@ -594,29 +594,34 @@ class rcube_vcard 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(); + $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard); + $lines = explode("\n", $vcard); + $result = array(); for ($i=0; $i < count($lines); $i++) { - if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line)) + if (!($pos = strpos($lines[$i], ':'))) { continue; + } + + $prefix = substr($lines[$i], 0, $pos); + $data = substr($lines[$i], $pos+1); - if (preg_match('/^(BEGIN|END)$/i', $line[1])) + if (preg_match('/^(BEGIN|END)$/i', $prefix)) { 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) + if ($result['VERSION'][0] == "2.1" + && preg_match('/^([^;]+);([^:]+)/', $prefix, $regs2) && !preg_match('/^TYPE=/i', $regs2[2]) ) { - $line[1] = $regs2[1]; + $prefix = $regs2[1]; foreach (explode(';', $regs2[2]) as $prop) { - $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop); + $prefix .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop); } } - if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) { + if (preg_match_all('/([^\\;]+);?/', $prefix, $regs2)) { $entry = array(); $field = strtoupper($regs2[1][0]); $enc = null; @@ -629,10 +634,10 @@ class rcube_vcard // 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]; + $data .= "\n" . $lines[++$i]; } } - $enc = $value; + $enc = $value == 'BASE64' ? 'B' : $value; } else { $lc_key = strtolower($key); @@ -652,20 +657,30 @@ class rcube_vcard // should we use vCard 3.0 instead? // $entry['base64'] = true; } - $line[2] = self::decode_value($line[2], $enc ? $enc : 'base64'); + + $data = self::decode_value($data, $enc ? $enc : 'base64'); + } + else if ($field == 'PHOTO') { + // vCard 4.0 data URI, "PHOTO:data:image/jpeg;base64,..." + if (preg_match('/^data:[a-z\/_-]+;base64,/i', $data, $m)) { + $entry['encoding'] = $enc = 'B'; + $data = substr($data, strlen($m[0])); + $data = self::decode_value($data, 'base64'); + } } if ($enc != 'B' && empty($entry['base64'])) { - $line[2] = self::vcard_unquote($line[2]); + $data = self::vcard_unquote($data); } - $entry = array_merge($entry, (array) $line[2]); - $data[$field][] = $entry; + $entry = array_merge($entry, (array) $data); + $result[$field][] = $entry; } } - unset($data['VERSION']); - return $data; + unset($result['VERSION']); + + return $result; } /** diff --git a/program/lib/Roundcube/rcube_washtml.php b/program/lib/Roundcube/rcube_washtml.php index e23e5b21d..97ab56cdf 100644 --- a/program/lib/Roundcube/rcube_washtml.php +++ b/program/lib/Roundcube/rcube_washtml.php @@ -95,6 +95,7 @@ class rcube_washtml 'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img', + 'video', 'source', // form elements 'button', 'input', 'textarea', 'select', 'option', 'optgroup' ); @@ -206,7 +207,7 @@ class rcube_washtml $value .= ' ' . $val; // #1488535: Fix size units, so width:800 would be changed to width:800px - if (preg_match('/(left|right|top|bottom|width|height)/i', $cssid) + if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid) && preg_match('/^[0-9]+$/', $val) ) { $value .= 'px'; @@ -246,7 +247,10 @@ class rcube_washtml $quot = strpos($style, '"') !== false ? "'" : '"'; $t .= ' style=' . $quot . $style . $quot; } - else if ($key == 'background' || ($key == 'src' && strtolower($node->tagName) == 'img')) { //check tagName anyway + else if ($key == 'background' + || ($key == 'src' && preg_match('/^(img|source)$/i', $node->tagName)) + || ($key == 'poster' && strtolower($node->tagName) == 'video') + ) { if (($src = $this->config['cid_map'][$value]) || ($src = $this->config['cid_map'][$this->config['base_url'].$value]) ) { @@ -374,7 +378,7 @@ class rcube_washtml $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level'); // Use optimizations if supported - if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + if (PHP_VERSION_ID >= 50400) { @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT); } else { @@ -456,7 +460,7 @@ class rcube_washtml // Remove invalid HTML comments (#1487759) // Don't remove valid conditional comments // Don't remove MSOutlook (<!-->) conditional comments (#1489004) - $html = preg_replace('/<!--[^->\[\n]+>/', '', $html); + $html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html); // fix broken nested lists self::fix_broken_lists($html); |