alecpl
2011-10-19 c041d57036d4a30730408c8fbba2d4e12778d2d5
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;
@@ -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;
    }
@@ -448,68 +470,215 @@
            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 entries (if called recursively)
    * @return array Accumulated group members
     */
    function list_group_members($dn, $count = false, $entries = null)
    {
        $group_members = array();
        // fetch group object
        if (empty($entries)) {
            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
            if ($result === false)
            {
                $this->_debug("S: ".ldap_error($this->conn));
                return $group_members;
            }
            $entries = @ldap_get_entries($this->conn, $result);
        }
        for ($i=0; $i < $entries["count"]; $i++)
        {
            $entry = $entries[$i];
            if (empty($entry['objectclass']))
                continue;
            foreach ((array)$entry['objectclass'] as $objectclass)
            {
                switch (strtolower($objectclass)) {
                    case "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 = $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'], $count))
                    $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 +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);
    }
@@ -966,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];
@@ -982,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)
@@ -993,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
            {
@@ -1005,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));
@@ -1149,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;
        }
    }
@@ -1183,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));
@@ -1201,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;
    }
    /**
@@ -1288,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'];
@@ -1321,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'];
@@ -1355,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'];
@@ -1392,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'];
@@ -1477,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
@@ -1504,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');