From a3b85d7b8560cdc1057fcaffa3acbd247b4b5b7a Mon Sep 17 00:00:00 2001
From: Thomas B. <thomas@roundcube.net>
Date: Mon, 07 Oct 2013 13:19:03 -0400
Subject: [PATCH] Merge pull request #133 from cwickert/release-0.9-canned-responses

---
 program/lib/Roundcube/rcube_ldap.php |  288 +++++++++++++++++++++++++++++---------------------------
 1 files changed, 149 insertions(+), 139 deletions(-)

diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index e3ba8c2..7c40023 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -2,8 +2,6 @@
 
 /*
  +-----------------------------------------------------------------------+
- | program/include/rcube_ldap.php                                        |
- |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
  | Copyright (C) 2006-2012, The Roundcube Dev Team                       |
  | Copyright (C) 2011-2012, Kolab Systems AG                             |
@@ -14,14 +12,12 @@
  |                                                                       |
  | PURPOSE:                                                              |
  |   Interface to an LDAP address directory                              |
- |                                                                       |
  +-----------------------------------------------------------------------+
  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
  |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
  |         Aleksander Machniak <machniak@kolabsys.com>                   |
  +-----------------------------------------------------------------------+
 */
-
 
 /**
  * Model class to access an LDAP address directory
@@ -218,15 +214,16 @@
         if (empty($this->prop['ldap_version']))
             $this->prop['ldap_version'] = 3;
 
-        foreach ($this->prop['hosts'] as $host)
-        {
+        // try to connect + bind for every host configured
+        // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
+        // see http://www.php.net/manual/en/function.ldap-connect.php
+        foreach ($this->prop['hosts'] as $host) {
             $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
             $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
 
             $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
 
-            if ($lc = @ldap_connect($host, $this->prop['port']))
-            {
+            if ($lc = @ldap_connect($host, $this->prop['port'])) {
                 if ($this->prop['use_tls'] === true)
                     if (!ldap_start_tls($lc))
                         continue;
@@ -237,17 +234,117 @@
                 $this->prop['host'] = $host;
                 $this->conn = $lc;
 
+                if (!empty($this->prop['network_timeout']))
+                  ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']);
+
                 if (isset($this->prop['referrals']))
                     ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
+            }
+            else {
+                $this->_debug("S: NOT OK");
+                continue;
+            }
+
+            // See if the directory is writeable.
+            if ($this->prop['writable']) {
+                $this->readonly = false;
+            }
+
+            $bind_pass = $this->prop['bind_pass'];
+            $bind_user = $this->prop['bind_user'];
+            $bind_dn   = $this->prop['bind_dn'];
+
+            $this->base_dn        = $this->prop['base_dn'];
+            $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
+            $this->prop['groups']['base_dn'] : $this->base_dn;
+
+            // User specific access, generate the proper values to use.
+            if ($this->prop['user_specific']) {
+                // No password set, use the session password
+                if (empty($bind_pass)) {
+                    $bind_pass = $rcube->get_user_password();
+                }
+
+                // Get the pieces needed for variable replacement.
+                if ($fu = $rcube->get_user_email())
+                    list($u, $d) = explode('@', $fu);
+                else
+                    $d = $this->mail_domain;
+
+                $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
+
+                $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
+
+                if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
+                    if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
+                        $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
+                    }
+
+                    // Search for the dn to use to authenticate
+                    $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
+                    $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
+
+                    $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
+
+                    $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
+                    if ($res) {
+                        if (($entry = ldap_first_entry($this->conn, $res))
+                            && ($bind_dn = ldap_get_dn($this->conn, $entry))
+                        ) {
+                            $this->_debug("S: search returned dn: $bind_dn");
+                            $dn = ldap_explode_dn($bind_dn, 1);
+                            $replaces['%dn'] = $dn[0];
+                        }
+                    }
+                    else {
+                        $this->_debug("S: ".ldap_error($this->conn));
+                    }
+
+                    // DN not found
+                    if (empty($replaces['%dn'])) {
+                        if (!empty($this->prop['search_dn_default']))
+                            $replaces['%dn'] = $this->prop['search_dn_default'];
+                        else {
+                            rcube::raise_error(array(
+                                'code' => 100, 'type' => 'ldap',
+                                'file' => __FILE__, 'line' => __LINE__,
+                                'message' => "DN not found using LDAP search."), true);
+                            return false;
+                        }
+                    }
+                }
+
+                // Replace the bind_dn and base_dn variables.
+                $bind_dn              = strtr($bind_dn, $replaces);
+                $this->base_dn        = strtr($this->base_dn, $replaces);
+                $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
+
+                if (empty($bind_user)) {
+                    $bind_user = $u;
+                }
+            }
+
+            if (empty($bind_pass)) {
+                $this->ready = true;
+            }
+            else {
+                if (!empty($bind_dn)) {
+                    $this->ready = $this->bind($bind_dn, $bind_pass);
+                }
+                else if (!empty($this->prop['auth_cid'])) {
+                    $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
+                }
+                else {
+                    $this->ready = $this->sasl_bind($bind_user, $bind_pass);
+                }
+            }
+
+            // connection established, we're done here
+            if ($this->ready) {
                 break;
             }
-            $this->_debug("S: NOT OK");
-        }
 
-        // See if the directory is writeable.
-        if ($this->prop['writable']) {
-            $this->readonly = false;
-        }
+        }  // end foreach hosts
 
         if (!is_resource($this->conn)) {
             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
@@ -255,95 +352,6 @@
                 'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
 
             return false;
-        }
-
-        $bind_pass = $this->prop['bind_pass'];
-        $bind_user = $this->prop['bind_user'];
-        $bind_dn   = $this->prop['bind_dn'];
-
-        $this->base_dn        = $this->prop['base_dn'];
-        $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
-        $this->prop['groups']['base_dn'] : $this->base_dn;
-
-        // User specific access, generate the proper values to use.
-        if ($this->prop['user_specific']) {
-            // No password set, use the session password
-            if (empty($bind_pass)) {
-                $bind_pass = $rcube->decrypt($_SESSION['password']);
-            }
-
-            // Get the pieces needed for variable replacement.
-            if ($fu = $rcube->get_user_email())
-                list($u, $d) = explode('@', $fu);
-            else
-                $d = $this->mail_domain;
-
-            $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
-
-            $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
-
-            if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
-                if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
-                    $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
-                }
-
-                // Search for the dn to use to authenticate
-                $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
-                $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
-
-                $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
-
-                $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
-                if ($res) {
-                    if (($entry = ldap_first_entry($this->conn, $res))
-                        && ($bind_dn = ldap_get_dn($this->conn, $entry))
-                    ) {
-                        $this->_debug("S: search returned dn: $bind_dn");
-                        $dn = ldap_explode_dn($bind_dn, 1);
-                        $replaces['%dn'] = $dn[0];
-                    }
-                }
-                else {
-                    $this->_debug("S: ".ldap_error($this->conn));
-                }
-
-                // DN not found
-                if (empty($replaces['%dn'])) {
-                    if (!empty($this->prop['search_dn_default']))
-                        $replaces['%dn'] = $this->prop['search_dn_default'];
-                    else {
-                        rcube::raise_error(array(
-                            'code' => 100, 'type' => 'ldap',
-                            'file' => __FILE__, 'line' => __LINE__,
-                            'message' => "DN not found using LDAP search."), true);
-                        return false;
-                    }
-                }
-            }
-
-            // Replace the bind_dn and base_dn variables.
-            $bind_dn              = strtr($bind_dn, $replaces);
-            $this->base_dn        = strtr($this->base_dn, $replaces);
-            $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
-
-            if (empty($bind_user)) {
-                $bind_user = $u;
-            }
-        }
-
-        if (empty($bind_pass)) {
-            $this->ready = true;
-        }
-        else {
-            if (!empty($bind_dn)) {
-                $this->ready = $this->bind($bind_dn, $bind_pass);
-            }
-            else if (!empty($this->prop['auth_cid'])) {
-                $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
-            }
-            else {
-                $this->ready = $this->sasl_bind($bind_user, $bind_pass);
-            }
         }
 
         return $this->ready;
@@ -798,27 +806,14 @@
             $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
 
             // get all entries of this page and post-filter those that really match the query
-            $search = mb_strtolower($value);
+            $search  = mb_strtolower($value);
             $entries = ldap_get_entries($this->conn, $this->ldap_result);
 
             for ($i = 0; $i < $entries['count']; $i++) {
                 $rec = $this->_ldap2result($entries[$i]);
                 foreach ($fields as $f) {
                     foreach ((array)$rec[$f] as $val) {
-                        $val = mb_strtolower($val);
-                        switch ($mode) {
-                        case 1:
-                            $got = ($val == $search);
-                            break;
-                        case 2:
-                            $got = ($search == substr($val, 0, strlen($search)));
-                            break;
-                        default:
-                            $got = (strpos($val, $search) !== false);
-                            break;
-                        }
-
-                        if ($got) {
+                        if ($this->compare_search_value($f, $val, $search, $mode)) {
                             $this->result->add($rec);
                             $this->result->count++;
                             break 2;
@@ -876,6 +871,8 @@
         // add required (non empty) fields filter
         $req_filter = '';
         foreach ((array)$required as $field) {
+            if (in_array($field, (array)$fields))  // required field is already in search filter
+                continue;
             if ($attrs = $this->_map_field($field)) {
                 if (count($attrs) > 1)
                     $req_filter .= '(|';
@@ -1408,13 +1405,15 @@
 
         foreach ((array)$this->prop['autovalues'] as $lf => $templ) {
             if (empty($attrs[$lf])) {
-                // replace {attr} placeholders with concrete attribute values
-                $templ = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
-
-                if (strpos($templ, '(') !== false)
-                    $attrs[$lf] = eval("return ($templ);");
-                else
-                    $attrs[$lf] = $templ;
+                if (strpos($templ, '(') !== false) {
+                    // replace {attr} placeholders with (escaped!) attribute values to be safely eval'd
+                    $code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals)));
+                    $attrs[$lf] = eval("return ($code);");
+                }
+                else {
+                    // replace {attr} placeholders with concrete attribute values
+                    $attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
+                }
             }
         }
     }
@@ -1455,6 +1454,7 @@
                 if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
                     if (ldap_parse_result($this->conn, $this->ldap_result,
                         $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
+                        && $serverctrls // can be null e.g. in case of adm. limit error
                     ) {
                         ldap_parse_virtuallist_control($this->conn, $serverctrls,
                             $last_offset, $this->vlv_count, $vresult);
@@ -1719,9 +1719,14 @@
      * List all active contact groups of this source
      *
      * @param string  Optional search string to match group name
+     * @param int     Matching mode:
+     *                0 - partial (*abc*),
+     *                1 - strict (=),
+     *                2 - prefix (abc*)
+     *
      * @return array  Indexed list of contact groups, each a hash array
      */
-    function list_groups($search = null)
+    function list_groups($search = null, $mode = 0)
     {
         if (!$this->groups)
             return array();
@@ -1733,10 +1738,10 @@
 
         $groups = array();
         if ($search) {
-            $search = mb_strtolower($search);
             foreach ($group_cache as $group) {
-                if (strpos(mb_strtolower($group['name']), $search) !== false)
+                if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
                     $groups[] = $group;
+                }
             }
         }
         else
@@ -1925,9 +1930,10 @@
     /**
      * Add the given contact records the a certain group
      *
-     * @param string  Group identifier
-     * @param array   List of contact identifiers to be added
-     * @return int    Number of contacts added
+     * @param string       Group identifier
+     * @param array|string List of contact identifiers to be added
+     *
+     * @return int Number of contacts added
      */
     function add_to_group($group_id, $contact_ids)
     {
@@ -1941,8 +1947,8 @@
         $group_name  = $group_cache[$group_id]['name'];
         $member_attr = $group_cache[$group_id]['member_attr'];
         $group_dn    = "cn=$group_name,$base_dn";
+        $new_attrs   = array();
 
-        $new_attrs = array();
         foreach ($contact_ids as $id)
             $new_attrs[$member_attr][] = self::dn_decode($id);
 
@@ -1953,28 +1959,32 @@
 
         $this->cache->remove('groups');
 
-        return count($new_attrs['member']);
+        return count($new_attrs[$member_attr]);
     }
 
     /**
      * Remove the given contact records from a certain group
      *
-     * @param string  Group identifier
-     * @param array   List of contact identifiers to be removed
-     * @return int    Number of deleted group members
+     * @param string       Group identifier
+     * @param array|string List of contact identifiers to be removed
+     *
+     * @return int Number of deleted group members
      */
     function remove_from_group($group_id, $contact_ids)
     {
         if (($group_cache = $this->cache->get('groups')) === null)
             $group_cache = $this->_fetch_groups();
 
+        if (!is_array($contact_ids))
+            $contact_ids = explode(',', $contact_ids);
+
         $base_dn     = $this->groups_base_dn;
         $group_name  = $group_cache[$group_id]['name'];
         $member_attr = $group_cache[$group_id]['member_attr'];
         $group_dn    = "cn=$group_name,$base_dn";
+        $del_attrs   = array();
 
-        $del_attrs = array();
-        foreach (explode(",", $contact_ids) as $id)
+        foreach ($contact_ids as $id)
             $del_attrs[$member_attr][] = self::dn_decode($id);
 
         if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
@@ -1984,7 +1994,7 @@
 
         $this->cache->remove('groups');
 
-        return count($del_attrs['member']);
+        return count($del_attrs[$member_attr]);
     }
 
     /**

--
Gitblit v1.9.1