From c041d57036d4a30730408c8fbba2d4e12778d2d5 Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Wed, 19 Oct 2011 05:56:06 -0400
Subject: [PATCH] - Added 'search_dn_default' variable in ldap config - Better handling of situation when search for bind DN doesn't return data

---
 program/include/rcube_ldap.php |  749 ++++++++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 520 insertions(+), 229 deletions(-)

diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 55c8c15..1f03e15 100644
--- a/program/include/rcube_ldap.php
+++ b/program/include/rcube_ldap.php
@@ -54,7 +54,6 @@
     private $base_dn = '';
     private $groups_base_dn = '';
     private $group_url = null;
-    private $group_members = array();
     private $cache;
 
     private $vlv_active = false;
@@ -72,7 +71,7 @@
     function __construct($p, $debug=false, $mail_domain=NULL)
     {
         $this->prop = $p;
-        
+
         if (isset($p['searchonly']))
             $this->searchonly = $p['searchonly'];
 
@@ -87,6 +86,8 @@
             // set default name attribute to cn
             if (empty($this->prop['groups']['name_attr']))
                 $this->prop['groups']['name_attr'] = 'cn';
+            if (empty($this->prop['groups']['scope']))
+                $this->prop['groups']['scope'] = 'sub';
         }
 
         // fieldmap property is given
@@ -135,7 +136,6 @@
         // initialize cache
         $rcmail = rcmail::get_instance();
         $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
-        $this->cache->expunge();
 
         $this->_connect();
     }
@@ -165,12 +165,14 @@
 
         foreach ($this->prop['hosts'] as $host)
         {
-            $host = idn_to_ascii(rcube_parse_host($host));
-            $this->_debug("C: Connect [$host".($this->prop['port'] ? ':'.$this->prop['port'] : '')."]");
+            $host     = idn_to_ascii(rcube_parse_host($host));
+            $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
+
+            $this->_debug("C: Connect [$hostname]");
 
             if ($lc = @ldap_connect($host, $this->prop['port']))
             {
-                if ($this->prop['use_tls']===true)
+                if ($this->prop['use_tls'] === true)
                     if (!ldap_start_tls($lc))
                         continue;
 
@@ -184,85 +186,105 @@
             $this->_debug("S: NOT OK");
         }
 
-        if (is_resource($this->conn))
-        {
-            $this->ready = true;
-
-            $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 = $RCMAIL->decrypt($_SESSION['password']);
-                }
-
-                // Get the pieces needed for variable replacement.
-                if ($fu = $RCMAIL->user->get_username())
-                  list($u, $d) = explode('@', $fu);
-                else
-                  $d = $this->mail_domain;
-
-                $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
-
-                $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
-
-                if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
-                    // 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 && ($entry = ldap_first_entry($this->conn, $res))) {
-                        $bind_dn = ldap_get_dn($this->conn, $entry);
-
-                        $this->_debug("S: search returned dn: $bind_dn");
-
-                        if ($bind_dn) {
-                            $dn = ldap_explode_dn($bind_dn, 1);
-                            $replaces['%dn'] = $dn[0];
-                        }
-                    }
-                }
-                // 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)) {
-                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);
-                }
-            }
-        }
-        else
-            raise_error(array('code' => 100, 'type' => 'ldap',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "Could not connect to any LDAP server, last tried $host:{$this->prop[port]}"), true);
-
         // See if the directory is writeable.
         if ($this->prop['writable']) {
             $this->readonly = false;
-        } // end if
+        }
+
+        if (!is_resource($this->conn)) {
+            raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
+
+            return false;
+        }
+
+        $bind_pass = $this->prop['bind_pass'];
+        $bind_user = $this->prop['bind_user'];
+        $bind_dn   = $this->prop['bind_dn'];
+
+        $this->base_dn        = $this->prop['base_dn'];
+        $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
+        $this->prop['groups']['base_dn'] : $this->base_dn;
+
+        // User specific access, generate the proper values to use.
+        if ($this->prop['user_specific']) {
+            // No password set, use the session password
+            if (empty($bind_pass)) {
+                $bind_pass = $RCMAIL->decrypt($_SESSION['password']);
+            }
+
+            // Get the pieces needed for variable replacement.
+            if ($fu = $RCMAIL->user->get_username())
+                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']) {
+                // 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 {
+                        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;
     }
 
 
@@ -447,72 +469,216 @@
             $this->result->searchonly = true;
             return $this->result;
         }
-        
-        // add general filter to query
-        if (!empty($this->prop['filter']) && empty($this->filter))
+
+        // fetch group members recursively
+        if ($this->group_id && $this->group_data['dn'])
         {
-            $filter = $this->prop['filter'];
-            $this->set_search_set($filter);
+            $entries = $this->list_group_members($this->group_data['dn']);
+
+            // make list of entries unique and sort it
+            $seen = array();
+            foreach ($entries as $i => $rec) {
+                if ($seen[$rec['dn']]++)
+                    unset($entries[$i]);
+            }
+            usort($entries, array($this, '_entry_sort_cmp'));
+
+            $entries['count'] = count($entries);
+            $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
+        }
+        else
+        {
+            // add general filter to query
+            if (!empty($this->prop['filter']) && empty($this->filter))
+                $this->set_search_set($this->prop['filter']);
+
+            // exec LDAP search if no result resource is stored
+            if ($this->conn && !$this->ldap_result)
+                $this->_exec_search();
+
+            // count contacts for this user
+            $this->result = $this->count();
+
+            // we have a search result resource
+            if ($this->ldap_result && $this->result->count > 0)
+            {
+                // sorting still on the ldap server
+                if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
+                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
+
+                // get all entries from the ldap server
+                $entries = ldap_get_entries($this->conn, $this->ldap_result);
+            }
+
+        }  // end else
+
+        // start and end of the page
+        $start_row = $this->vlv_active ? 0 : $this->result->first;
+        $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
+        $last_row = $this->result->first + $this->page_size;
+        $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
+
+        // filter entries for this page
+        for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
+            $this->result->add($this->_ldap2result($entries[$i]));
+
+        return $this->result;
+    }
+
+    /**
+    * Get all members of the given group
+    *
+    * @param string Group DN
+    * @param array  Group entries (if called recursively)
+    * @return array Accumulated group members
+     */
+    function list_group_members($dn, $count = false, $entries = null)
+    {
+        $group_members = array();
+
+        // fetch group object
+        if (empty($entries)) {
+            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
+            if ($result === false)
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return $group_members;
+            }
+
+            $entries = @ldap_get_entries($this->conn, $result);
         }
 
-        // query URL is given by the selected group
-        if ($this->group_id && $this->group_url)
+        for ($i=0; $i < $entries["count"]; $i++)
+        {
+            $entry = $entries[$i];
+
+            if (empty($entry['objectclass']))
+                continue;
+
+            foreach ((array)$entry['objectclass'] as $objectclass)
+            {
+                switch (strtolower($objectclass)) {
+                    case "groupofnames":
+                    case "kolabgroupofnames":
+                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
+                        break;
+                    case "groupofuniquenames":
+                    case "kolabgroupofuniquenames":
+                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
+                        break;
+                    case "groupofurls":
+                        $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
+                        break;
+                }
+            }
+            
+            if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
+              break;
+        }
+
+        return array_filter($group_members);
+    }
+
+    /**
+     * Fetch members of the given group entry from server
+     *
+     * @param string Group DN
+     * @param array  Group entry
+     * @param string Member attribute to use
+     * @return array Accumulated group members
+     */
+    private function _list_group_members($dn, $entry, $attr, $count)
+    {
+        // Use the member attributes to return an array of member ldap objects
+        // NOTE that the member attribute is supposed to contain a DN
+        $group_members = array();
+        if (empty($entry[$attr]))
+            return $group_members;
+
+        // read these attributes for all members
+        $attrib = $count ? array('dn') : array_values($this->fieldmap);
+        $attrib[] = 'objectClass';
+        $attrib[] = 'member';
+        $attrib[] = 'uniqueMember';
+        $attrib[] = 'memberURL';
+
+        for ($i=0; $i < $entry[$attr]['count']; $i++)
+        {
+            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
+                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
+
+            $members = @ldap_get_entries($this->conn, $result);
+            if ($members == false)
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                $members = array();
+            }
+
+            // for nested groups, call recursively
+            $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
+
+            unset($members['count']);
+            $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
+        }
+
+        return $group_members;
+    }
+
+    /**
+     * List members of group class groupOfUrls
+     *
+     * @param string Group DN
+     * @param array  Group entry
+     * @param boolean True if only used for counting
+     * @return array Accumulated group members
+     */
+    private function _list_group_memberurl($dn, $entry, $count)
+    {
+        $group_members = array();
+
+        for ($i=0; $i < $entry['memberurl']['count']; $i++)
         {
             // extract components from url
-            if (preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $this->group_url, $m))
+            if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
+                continue;
+
+            // add search filter if any
+            $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
+            $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
+
+            $attrib = $count ? array('dn') : array_values($this->fieldmap);
+            if ($result = @$func($this->conn, $m[1], $filter,
+                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
             {
-                $this->base_dn = $m[1];
-                $this->prop['scope'] = $m[2];
-                $this->filter = $m[3];
+                $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
+                if ($err = ldap_errno($this->conn))
+                    $this->_debug("S: Error: " .ldap_err2str($err));
+            }
+            else
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return $group_members;
+            }
+
+            $entries = @ldap_get_entries($this->conn, $result);
+            for ($j = 0; $j < $entries['count']; $j++)
+            {
+                if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
+                    $group_members = array_merge($group_members, $nested_group_members);
+                else
+                    $group_members[] = $entries[$j];
             }
         }
 
-        // exec LDAP search if no result resource is stored
-        if ($this->conn && !$this->ldap_result)
-            $this->_exec_search();
+        return $group_members;
+    }
 
-        // count contacts for this user
-        $this->result = $this->count();
-
-        // we have a search result resource
-        if ($this->ldap_result && $this->result->count > 0)
-        {
-            // sorting still on the ldap server
-            if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
-                ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
-
-            // start and end of the page
-            $start_row = $this->vlv_active ? 0 : $this->result->first;
-            $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
-            $last_row = $this->result->first + $this->page_size;
-            $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
-
-            // get all entries from the ldap server
-            $entries = ldap_get_entries($this->conn, $this->ldap_result);
-
-            // filtering for group members
-            if ($this->groups && $this->group_id && !$this->group_url)
-            {
-                $count = 0;
-                $members = array();
-                foreach ($entries as $entry)
-                {
-                    if ($this->group_members[self::dn_encode($entry['dn'])])
-                    {
-                        $members[] = $entry;
-                        $count++;
-                    }
-                }
-                $entries = $members;
-                $entries['count'] = $count;
-                $this->result->count = $count;
-            }
-
-            // filter entries for this page
-            for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
-                $this->result->add($this->_ldap2result($entries[$i]));
-        }
-        return $this->result;
+    /**
+     * Callback for sorting entries
+     */
+    function _entry_sort_cmp($a, $b)
+    {
+        return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
     }
 
 
@@ -616,18 +782,21 @@
         $count = 0;
         if ($this->conn && $this->ldap_result) {
             $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
-        } // end if
-        elseif ($this->conn) {
+        }
+        else if ($this->group_id && $this->group_data['dn']) {
+            $count = count($this->list_group_members($this->group_data['dn'], true));
+        }
+        else if ($this->conn) {
             // We have a connection but no result set, attempt to get one.
             if (empty($this->filter)) {
                 // The filter is not set, set it.
                 $this->filter = $this->prop['filter'];
-            } // end if
+            }
             $this->_exec_search(true);
             if ($this->ldap_result) {
                 $count = ldap_count_entries($this->conn, $this->ldap_result);
-            } // end if
-        } // end else
+            }
+        }
 
         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
     }
@@ -689,19 +858,49 @@
      * If input not valid, the message to display can be fetched using get_error()
      *
      * @param array Assoziative array with data to save
-     *
+     * @param boolean Try to fix/complete record automatically
      * @return boolean True if input is valid, False if not.
      */
-    public function validate($save_data)
+    public function validate(&$save_data, $autofix = false)
     {
         // check for name input
         if (empty($save_data['name'])) {
-            $this->set_error('warning', 'nonamewarning');
+            $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
+            return false;
+        }
+
+        // Verify that the required fields are set.
+        $missing = null;
+        $ldap_data = $this->_map_data($save_data);
+        foreach ($this->prop['required_fields'] as $fld) {
+            if (!isset($ldap_data[$fld])) {
+                $missing[$fld] = 1;
+            }
+        }
+
+        if ($missing) {
+            // try to complete record automatically
+            if ($autofix) {
+                $reverse_map = array_flip($this->fieldmap);
+                $name_parts = preg_split('/[\s,.]+/', $save_data['name']);
+                if ($missing['sn']) {
+                    $sn_field = $reverse_map['sn'];
+                    $save_data[$sn_field] = array_pop ($name_parts);
+                }
+                if ($missing[($fn_field = $this->fieldmap['firstname'])]) {
+                    $save_data['firstname'] = array_shift($name_parts);
+                }
+
+                return $this->validate($save_data, false);
+            }
+
+            // TODO: generate message saying which fields are missing
+            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
             return false;
         }
 
         // validate e-mail addresses
-        return parent::validate($save_data);
+        return parent::validate($save_data, $autofix);
     }
 
 
@@ -715,17 +914,8 @@
     function insert($save_cols)
     {
         // Map out the column names to their LDAP ones to build the new entry.
-        $newentry = array();
+        $newentry = $this->_map_data($save_cols);
         $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
-        foreach ($this->fieldmap as $col => $fld) {
-            $val = $save_cols[$col];
-            if (is_array($val))
-                $val = array_filter($val);  // remove empty entries
-            if ($fld && $val) {
-                // The field does exist, add it to the entry.
-                $newentry[$fld] = $val;
-            } // end if
-        } // end foreach
 
         // Verify that the required fields are set.
         $missing = null;
@@ -738,7 +928,7 @@
         // abort process if requiered fields are missing
         // TODO: generate message saying which fields are missing
         if ($missing) {
-            $this->set_error(self::ERROR_INCOMPLETE, 'formincomplete');
+            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
             return false;
         }
 
@@ -948,14 +1138,14 @@
         if ($this->ready)
         {
             $filter = $this->filter ? $this->filter : '(objectclass=*)';
-            $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
+            $function = $this->_scope2func($this->prop['scope'], $ns_function);
 
-            $this->_debug("C: Search [$filter]");
+            $this->_debug("C: Search [$filter][dn: $this->base_dn]");
 
             // when using VLV, we get the total count by...
             if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
                 // ...either reading numSubOrdinates attribute
-                if ($this->prop['numsub_filter'] && ($result_count = @$function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
+                if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
                     $counts = ldap_get_entries($this->conn, $result_count);
                     for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
                         $this->vlv_count += $counts[$j]['numsubordinates'][0];
@@ -964,7 +1154,7 @@
                 else  // ...or by fetching all records dn and count them
                     $this->vlv_count = $this->_exec_search(true);
 
-                $this->vlv_active = $this->_vlv_set_controls();
+                $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
             }
 
             // only fetch dn for count (should keep the payload low)
@@ -975,7 +1165,7 @@
                 $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
                 if ($err = ldap_errno($this->conn))
                     $this->_debug("S: Error: " .ldap_err2str($err));
-                return true;
+                return $count ? ldap_count_entries($this->conn, $this->ldap_result) : true;
             }
             else
             {
@@ -987,15 +1177,37 @@
     }
 
     /**
+     * Choose the right PHP function according to scope property
+     */
+    private function _scope2func($scope, &$ns_function = null)
+    {
+        switch ($scope) {
+          case 'sub':
+            $function = $ns_function  = 'ldap_search';
+            break;
+          case 'base':
+            $function = $ns_function = 'ldap_read';
+            break;
+          default:
+            $function = 'ldap_list';
+            $ns_function = 'ldap_read';
+            break;
+        }
+        
+        return $function;
+    }
+
+    /**
      * Set server controls for Virtual List View (paginated listing)
      */
-    private function _vlv_set_controls()
+    private function _vlv_set_controls($prop, $list_page, $page_size)
     {
-        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$this->prop['sort']));
-        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($this->list_page-1) * $this->page_size + 1), $this->page_size), 'iscritical' => true);
+        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$prop['sort']));
+        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size), 'iscritical' => true);
 
-        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ({$this->sort_col});"
-            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset)");
+        $sort = (array)$prop['sort'];
+        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
+            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
 
         if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
             $this->_debug("S: ".ldap_error($this->conn));
@@ -1031,6 +1243,11 @@
                 else
                     $out[$rf] = $value;
             }
+
+            // Make sure name fields aren't arrays (#1488108)
+            if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
+                $out[$rf] = $out[$rf][0];
+            }
         }
 
         return $out;
@@ -1043,6 +1260,26 @@
     private function _map_field($field)
     {
         return $this->fieldmap[$field];
+    }
+
+
+    /**
+     * Convert a record data set into LDAP field attributes
+     */
+    private function _map_data($save_cols)
+    {
+        $ldap_data = array();
+        foreach ($this->fieldmap as $col => $fld) {
+            $val = $save_cols[$col];
+            if (is_array($val))
+                $val = array_filter($val);  // remove empty entries
+            if ($fld && $val) {
+                // The field does exist, add it to the entry.
+                $ldap_data[$fld] = $val;
+            }
+        }
+        
+        return $ldap_data;
     }
 
 
@@ -1106,26 +1343,16 @@
     {
         if ($group_id)
         {
-            if (!($group_cache = $this->cache->get('groups')))
-                $group_cache = $this->list_groups();
+            if (($group_cache = $this->cache->get('groups')) === null)
+                $group_cache = $this->_fetch_groups();
 
-            $cache_members = $group_cache[$group_id]['members'];
-
-            $members = array();
-            for ($i=0; $i<$cache_members["count"]; $i++)
-            {
-                if (!empty($cache_members[$i]))
-                    $members[self::dn_encode($cache_members[$i])] = 1;
-            }
-            $this->group_members = $members;
-            $this->group_url = $group_cache[$group_id]['member_url'];
             $this->group_id = $group_id;
+            $this->group_data = $group_cache[$group_id];
         }
         else
         {
             $this->group_id = 0;
-            $this->group_url = null;
-            $this->group_members = array();
+            $this->group_data = null;
         }
     }
 
@@ -1140,13 +1367,49 @@
         if (!$this->groups)
             return array();
 
+        // use cached list for searching
+        $this->cache->expunge();
+        if (!$search || ($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $groups = array();
+        if ($search) {
+            $search = strtolower($search);
+            foreach ($group_cache as $group) {
+                if (strstr(strtolower($group['name']), $search))
+                    $groups[] = $group;
+            }
+        }
+        else
+            $groups = $group_cache;
+
+        return array_values($groups);
+    }
+
+    /**
+     * Fetch groups from server
+     */
+    private function _fetch_groups($vlv_page = 0)
+    {
         $base_dn = $this->groups_base_dn;
         $filter = $this->prop['groups']['filter'];
         $name_attr = $this->prop['groups']['name_attr'];
+        $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
+        $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
+        $sort_attr = $sort_attrs[0];
 
         $this->_debug("C: Search [$filter][dn: $base_dn]");
 
-        $res = @ldap_search($this->conn, $base_dn, $filter);
+        // use vlv to list groups
+        if ($this->prop['groups']['vlv']) {
+            $page_size = 200;
+            if (!$this->prop['groups']['sort'])
+                $this->prop['groups']['sort'] = $sort_attrs;
+            $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
+        }
+
+        $function = $this->_scope2func($this->prop['groups']['scope'], $ns_function);
+        $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
         if ($res === false)
         {
             $this->_debug("S: ".ldap_error($this->conn));
@@ -1158,49 +1421,77 @@
 
         $groups = array();
         $group_sortnames = array();
-        for ($i=0; $i<$ldap_data["count"]; $i++)
+        $group_count = $ldap_data["count"];
+        for ($i=0; $i < $group_count; $i++)
         {
-            $group_name = $ldap_data[$i][$name_attr][0];
-            if (!$search || strstr(strtolower($group_name), strtolower($search)))
-            {
-                $group_id = self::dn_encode($group_name);
-                $groups[$group_id]['ID'] = $group_id;
-                $groups[$group_id]['name'] = $group_name;
+            $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
+            $group_id = self::dn_encode($group_name);
+            $groups[$group_id]['ID'] = $group_id;
+            $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
+            $groups[$group_id]['name'] = $group_name;
+            $groups[$group_id]['member_attr'] = $this->prop['member_attr'];
 
-                // default behavior: check members attribute
-                if ($this->prop['member_attr']) {
-                    $groups[$group_id]['members'] = $ldap_data[$i][$this->prop['member_attr']];
-                    $groups[$group_id]['member_attr'] = $this->prop['member_attr'];
+            // check objectClass attributes of group and act accordingly
+            for ($j=0; $j < $ldap_data[$i]['objectclass']['count']; $j++) {
+                switch (strtolower($ldap_data[$i]['objectclass'][$j])) {
+                    case 'groupofnames':
+                    case 'kolabgroupofnames':
+                        $groups[$group_id]['member_attr'] = 'member';
+                        break;
+
+                    case 'groupofuniquenames':
+                    case 'kolabgroupofuniquenames':
+                        $groups[$group_id]['member_attr'] = 'uniqueMember';
+                        break;
                 }
-
-                // check objectClass attributes of group and act accordingly
-                for ($j=0; $j < $ldap_data[$i]['objectclass']['count']; $j++) {
-                    switch (strtolower($ldap_data[$i]['objectclass'][$j])) {
-                        case 'groupofnames':
-                            $groups[$group_id]['members'] = $ldap_data[$i]['members'];
-                            $groups[$group_id]['member_attr'] = 'member';
-                            break;
-
-                        case 'groupofuniquenames':
-                            $groups[$group_id]['members'] = $ldap_data[$i]['uniqueMember'];
-                            $groups[$group_id]['member_attr'] = 'uniqueMember';
-                            break;
-
-                        case 'groupofurls':
-                            $groups[$group_id]['member_url'] = $ldap_data[$i]['memberurl'][0];
-                            break;
-                    }
-                }
-
-                $group_sortnames[] = strtolower($group_name);
             }
+
+            // list email attributes of a group
+            for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
+                if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
+                    $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
+            }
+
+            $group_sortnames[] = strtolower($ldap_data[$i][$sort_attr][0]);
         }
-        array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
+
+        // recursive call can exit here
+        if ($vlv_page > 0)
+            return $groups;
+
+        // call recursively until we have fetched all groups
+        while ($vlv_active && $group_count == $page_size)
+        {
+            $next_page = $this->_fetch_groups(++$vlv_page);
+            $groups = array_merge($groups, $next_page);
+            $group_count = count($next_page);
+        }
+
+        // when using VLV the list of groups is already sorted
+        if (!$this->prop['groups']['vlv'])
+            array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
 
         // cache this
         $this->cache->set('groups', $groups);
 
         return $groups;
+    }
+
+    /**
+     * Get group properties such as name and email address(es)
+     *
+     * @param string Group identifier
+     * @return array Group properties as hash array
+     */
+    function get_group($group_id)
+    {
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
+
+        $group_data = $group_cache[$group_id];
+        unset($group_data['dn'], $group_data['member_attr']);
+
+        return $group_data;
     }
 
     /**
@@ -1245,8 +1536,8 @@
      */
     function delete_group($group_id)
     {
-        if (!($group_cache = $this->cache->get('groups')))
-            $group_cache = $this->list_groups();
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
 
         $base_dn = $this->groups_base_dn;
         $group_name = $group_cache[$group_id]['name'];
@@ -1278,8 +1569,8 @@
      */
     function rename_group($group_id, $new_name, &$new_gid)
     {
-        if (!($group_cache = $this->cache->get('groups')))
-            $group_cache = $this->list_groups();
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
 
         $base_dn = $this->groups_base_dn;
         $group_name = $group_cache[$group_id]['name'];
@@ -1312,8 +1603,8 @@
      */
     function add_to_group($group_id, $contact_ids)
     {
-        if (!($group_cache = $this->cache->get('groups')))
-            $group_cache = $this->list_groups();
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
 
         $base_dn     = $this->groups_base_dn;
         $group_name  = $group_cache[$group_id]['name'];
@@ -1349,8 +1640,8 @@
      */
     function remove_from_group($group_id, $contact_ids)
     {
-        if (!($group_cache = $this->cache->get('groups')))
-            $group_cache = $this->list_groups();
+        if (($group_cache = $this->cache->get('groups')) === null)
+            $group_cache = $this->_fetch_groups();
 
         $base_dn     = $this->groups_base_dn;
         $group_name  = $group_cache[$group_id]['name'];
@@ -1434,11 +1725,11 @@
         # 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
-        # 20 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
-        # 20 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
+        # 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
-        # 20 = type integer with 2 bytes following (offset): 01 01 (ie 1)
-        # 20 = type integer with 2 bytes following (contentCount):  01 00
+        # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
+        # 02 = type integer with 2 bytes following (contentCount):  01 00
         # 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
@@ -1461,7 +1752,7 @@
         $str = self::_ber_addseq($str, 'a0');
 
         // now tack on records per page
-        $str = sprintf("0201000201%02x", min(255, $rpp)-1) . $str;
+        $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');

--
Gitblit v1.9.1