From a3148236ebaa7e61177e7186138997d136b88b3e Mon Sep 17 00:00:00 2001
From: thomascube <thomas@roundcube.net>
Date: Mon, 10 Oct 2011 16:13:55 -0400
Subject: [PATCH] Rewrite groups listing: use recursive methods to list members of (nested) groups of various types; cache the results

---
 program/include/rcube_ldap.php |  409 +++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 292 insertions(+), 117 deletions(-)

diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 3af343f..d30262b 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;
@@ -135,7 +134,6 @@
         // initialize cache
         $rcmail = rcmail::get_instance();
         $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
-        $this->cache->expunge();
 
         $this->_connect();
     }
@@ -448,68 +446,203 @@
             return $this->result;
         }
 
-        // query URL is given by the selected group
-        if ($this->group_id && $this->group_url)
+        // fetch group members recursively
+        if ($this->group_id && $this->group_data['dn'])
+        {
+            $entries = $this->list_group_members($this->group_data['dn']);
+
+            // make list of entries unique and sort it
+            $seen = array();
+            foreach ($entries as $i => $rec) {
+                if ($seen[$rec['dn']]++)
+                    unset($entries[$i]);
+            }
+            usort($entries, array($this, '_entry_sort_cmp'));
+
+            $entries['count'] = count($entries);
+            $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
+        }
+        else
+        {
+            // add general filter to query
+            if (!empty($this->prop['filter']) && empty($this->filter))
+                $this->set_search_set($this->prop['filter']);
+
+            // exec LDAP search if no result resource is stored
+            if ($this->conn && !$this->ldap_result)
+                $this->_exec_search();
+
+            // count contacts for this user
+            $this->result = $this->count();
+
+            // we have a search result resource
+            if ($this->ldap_result && $this->result->count > 0)
+            {
+                // sorting still on the ldap server
+                if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
+                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
+
+                // get all entries from the ldap server
+                $entries = ldap_get_entries($this->conn, $this->ldap_result);
+            }
+
+        }  // end else
+
+        // start and end of the page
+        $start_row = $this->vlv_active ? 0 : $this->result->first;
+        $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
+        $last_row = $this->result->first + $this->page_size;
+        $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
+
+        // filter entries for this page
+        for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
+            $this->result->add($this->_ldap2result($entries[$i]));
+
+        return $this->result;
+    }
+
+    /**
+    * Get all members of the given group
+    *
+    * @param string Group DN
+    * @param array  Group entry (if called recursively)
+    * @return array Accumulated group members
+     */
+    function list_group_members($dn, $entries = null)
+    {
+        $group_members = array();
+
+        // fetch group object
+        if (empty($entries)) {
+            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
+            if ($result === false)
+            {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return $group_members;
+            }
+
+            $entries = @ldap_get_entries($this->conn, $result);
+        }
+
+        for ($i=0; $i < $entries["count"]; $i++)
+        {
+            $entry = $entries[$i];
+
+            if (empty($entry['objectclass']))
+                continue;
+
+            foreach ((array)$entry['objectclass'] as $num => $objectclass)
+            {
+                switch (strtolower($objectclass)) {
+                    case "groupofnames":
+                    case "kolabgroupofnames":
+                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member'));
+                        break;
+                    case "groupofuniquenames":
+                    case "kolabgroupofuniquenames":
+                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember'));
+                        break;
+                    case "groupofurls":
+                        $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry));
+                        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)
+    {
+        // 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;
+
+        for ($i=0; $i < $entry[$attr]['count']; $i++)
+        {
+            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
+                array_values($this->fieldmap), 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], $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
+     * @return array Accumulated group members
+     */
+    private function _list_group_memberurl($dn, $entry)
+    {
+        $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');
+
+            if ($result = @$func($this->conn, $m[1], $filter,
+                array_values($this->fieldmap), 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
             {
-                $this->base_dn = $m[1];
-                $this->prop['scope'] = $m[2];
-                $this->filter = $this->filter ? '(&(' . $m[3] . ')(' . $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']))
+                    $group_members = array_merge($group_members, $nested_group_members);
+                else
+                    $group_members[] = $entries[$j];
             }
         }
 
-        // add general filter to query
-        if (!empty($this->prop['filter']) && empty($this->filter))
-            $this->set_search_set($this->prop['filter']);
+        return $group_members;
+    }
 
-        // 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);
-
-            // 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]);
     }
 
 
@@ -613,18 +746,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']));
+        }
+        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);
     }
@@ -966,14 +1102,26 @@
         if ($this->ready)
         {
             $filter = $this->filter ? $this->filter : '(objectclass=*)';
-            $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
+
+            switch ($this->prop['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;
+            }
 
             $this->_debug("C: Search [$filter]");
 
             // 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];
@@ -1149,26 +1297,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;
         }
     }
 
@@ -1183,13 +1321,38 @@
         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()
+    {
         $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';
 
         $this->_debug("C: Search [$filter][dn: $base_dn]");
 
-        $res = @ldap_search($this->conn, $base_dn, $filter);
+        $res = @ldap_search($this->conn, $base_dn, $filter, array('dn', $name_attr, 'objectClass', $email_attr));
         if ($res === false)
         {
             $this->_debug("S: ".ldap_error($this->conn));
@@ -1204,39 +1367,34 @@
         for ($i=0; $i<$ldap_data["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_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($group_name);
         }
         array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
 
@@ -1244,6 +1402,23 @@
         $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;
     }
 
     /**
@@ -1288,8 +1463,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'];
@@ -1321,8 +1496,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'];
@@ -1355,8 +1530,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'];
@@ -1392,8 +1567,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'];

--
Gitblit v1.9.1