Thomas Bruederli
2013-02-05 ec2185a1cb2a0d412ab9e916618972bc4c70f68c
Add special config attribute 'group_filters' to specify a fixed list of groups to be listed in the leftmost pane, each having individual base-dn/filter parameters for contact listing
2 files modified
246 ■■■■■ changed files
program/include/rcmail.php 2 ●●● patch | view | raw | blame | history
program/lib/Roundcube/rcube_ldap.php 244 ●●●●● patch | view | raw | blame | history
program/include/rcmail.php
@@ -287,7 +287,7 @@
        $list[$id] = array(
          'id'       => $id,
          'name'     => html::quote($prop['name']),
          'groups'   => is_array($prop['groups']),
          'groups'   => !empty($prop['groups']) || !empty($prop['group_filters']),
          'readonly' => !$prop['writable'],
          'hidden'   => $prop['hidden'],
          'autocomplete' => in_array($id, $autocomplete)
program/lib/Roundcube/rcube_ldap.php
@@ -38,7 +38,7 @@
    /** private properties */
    protected $conn;
    protected $prop = array();
    protected $fieldmap = array();
    protected $fieldmap = array('_objclass' => 'objectclass');
    protected $sub_filter;
    protected $filter = '';
    protected $result = null;
@@ -82,6 +82,12 @@
                $this->prop['groups']['name_attr'] = 'cn';
            if (empty($this->prop['groups']['scope']))
                $this->prop['groups']['scope'] = 'sub';
            // add group name attrib to fieldmap in order to have it fetched
            $this->fieldmap['_groupname'] = $this->prop['groups']['name_attr'];
        }
        else if (is_array($p['group_filters']) && count($p['group_filters'])) {
            $this->groups = true;
        }
        // fieldmap property is given
@@ -549,13 +555,15 @@
        }
        else
        {
            $prop = $this->group_id ? $this->group_data : $this->prop;
            // add general filter to query
            if (!empty($this->prop['filter']) && empty($this->filter))
                $this->set_search_set($this->prop['filter']);
            if (!empty($prop['filter']) && empty($this->filter))
                $this->set_search_set($prop['filter']);
            // exec LDAP search if no result resource is stored
            if ($this->conn && !$this->ldap_result)
                $this->_exec_search();
                $this->_exec_search($prop);
            // count contacts for this user
            $this->result = $this->count();
@@ -564,7 +572,7 @@
            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)
                if ($this->sort_col && $prop['scope'] !== 'base' && !$this->vlv_active)
                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
                // get all entries from the ldap server
@@ -599,6 +607,7 @@
        // fetch group object
        if (empty($entries)) {
            $this->_debug("C: Read Group [dn: $dn]");
            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
            if ($result === false)
            {
@@ -657,21 +666,6 @@
        if (empty($entry[$attr]))
            return $group_members;
        // add group record to cache if it isn't yet there
        $group_id = self::dn_encode($dn);
        $group_cache = $this->cache->get('groups');
        if (!$group_cache[$group_id]) {
            $name_attr = $this->prop['groups']['name_attr'];
            $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
            $group_cache[$group_id]['ID'] = $group_id;
            $group_cache[$group_id]['dn'] = $dn;
            $group_cache[$group_id]['name'] = $group_name;
            $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry[$i]['objectclass']);
            $this->cache->set('groups', $group_cache);
        }
        // read these attributes for all members
        $attrib = $count ? array('dn') : array_values($this->fieldmap);
        $attrib[] = 'objectClass';
@@ -726,26 +720,15 @@
            // 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->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
            }
            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];
            $attrs = $count ? array('dn') : array_values($this->fieldmap);
            if ($result = $this->ldap_search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
                $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];
                }
            }
        }
@@ -910,7 +893,7 @@
        // set filter string and execute search
        $this->set_search_set($filter);
        $this->_exec_search();
        $this->_exec_search($this->prop);
        if ($select)
            $this->list_records();
@@ -936,13 +919,15 @@
            $count = count($this->list_group_members($this->group_data['dn'], true));
        }
        else if ($this->conn) {
            $prop = $this->group_id ? $this->group_data : $this->prop;
            // 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'];
            }
            $count = (int) $this->_exec_search(true);
            $count = (int) $this->_exec_search($prop, true);
        }
        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
@@ -1434,57 +1419,35 @@
    /**
     * Execute the LDAP search based on the stored credentials
     */
    private function _exec_search($count = false)
    private function _exec_search($prop, $count = false)
    {
        if ($this->ready)
        {
            $filter = $this->filter ? $this->filter : '(objectclass=*)';
            $function = $this->_scope2func($this->prop['scope'], $ns_function);
            $this->_debug("C: Search [$filter][dn: $this->base_dn]");
            $function = $this->_scope2func($prop['scope'], $ns_function);
            $base_dn = $this->group_id && $prop['base_dn'] ? $prop['base_dn'] : $this->base_dn;
            // when using VLV, we get the total count by...
            if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
            if (!$count && $function != 'ldap_read' && $prop['vlv']) {
                // ...either reading numSubOrdinates attribute
                if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
                if ($prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $base_dn, $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];
                    $this->_debug("D: total numsubordinates = " . $this->vlv_count);
                }
                else if (!function_exists('ldap_parse_virtuallist_control'))  // ...or by fetching all records dn and count them
                    $this->vlv_count = $this->_exec_search(true);
                $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
                    $this->vlv_count = $this->_exec_search($prop, true);
            }
            // only fetch dn for count (should keep the payload low)
            $attrs = $count ? array('dn') : array_values($this->fieldmap);
            if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
            ) {
                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
                    if (ldap_parse_result($this->conn, $this->ldap_result,
                        $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
                        && $serverctrls // can be null e.g. in case of adm. limit error
                    ) {
                        ldap_parse_virtuallist_control($this->conn, $serverctrls,
                            $last_offset, $this->vlv_count, $vresult);
                        $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
                    }
                    else {
                        $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
                    }
            $filter = $this->filter ? $this->filter : '(objectclass=*)';
            $attrs = $count ? array('dn') : array_values($this->fieldmap);  // only fetch dn for count (should keep the payload low)
            if ($this->ldap_result = $this->ldap_search($base_dn, $filter, $prop['scope'], $attrs, $prop)) {
                if ($count) {
                    return ldap_count_entries($this->conn, $this->ldap_result);
                }
                $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
                $this->_debug("S: $entries_count record(s)");
                return $count ? $entries_count : true;
            }
            else {
                $this->_debug("S: ".ldap_error($this->conn));
                else {
                    return true;
                }
            }
        }
@@ -1540,20 +1503,19 @@
    private function _ldap2result($rec)
    {
        $out = array('_type' => 'person');
        $fieldmap = $this->fieldmap;
        if ($rec['dn'])
            $out[$this->primary_key] = self::dn_encode($rec['dn']);
        // determine record type
        if (array_intersect(array('groupofuniquenames','kolabgroupofuniquenames'), (array)$rec['objectclass'])) {
        if (array_intersect(array('groupofuniquenames','kolabgroupofuniquenames'), array_map('strtolower', (array)$rec['objectclass']))) {
            $out['_type'] = 'group';
            $out['readonly'] = true;
            if ($this->fieldmap['groupname']) {
                $this->fieldmap['name'] = $this->fieldmap['groupname'];
            }
            $fieldmap['name'] = $fieldmap['_groupname'];
        }
        foreach ($this->fieldmap as $rf => $lf)
        foreach ($fieldmap as $rf => $lf)
        {
            for ($i=0; $i < $rec[$lf]['count']; $i++) {
                if (!($value = $rec[$lf][$i]))
@@ -1718,20 +1680,14 @@
    /**
     * Setter for the current group
     * (empty, has to be re-implemented by extending class)
     */
    function set_group($group_id)
    {
        if ($group_id)
        {
            if (($group_cache = $this->cache->get('groups')) === null)
                $group_cache = $this->_fetch_groups();
        if ($group_id) {
            $this->group_id = $group_id;
            $this->group_data = $group_cache[$group_id];
            $this->group_data = $this->get_group_entry($group_id);
        }
        else
        {
        else {
            $this->group_id = 0;
            $this->group_data = null;
        }
@@ -1772,6 +1728,23 @@
     */
    private function _fetch_groups($vlv_page = 0)
    {
        // special case: list groups from 'group_filters' config
        if (!empty($this->prop['group_filters'])) {
            $groups = array();
            // list regular groups configuration as special filter
            if (!empty($this->prop['groups']['filter'])) {
                $id = '__groups__';
                $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups')) + $this->prop['groups'];
            }
            foreach ($this->prop['group_filters'] as $id => $prop) {
                $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id));
            }
            return $groups;
        }
        $base_dn = $this->groups_base_dn;
        $filter = $this->prop['groups']['filter'];
        $name_attr = $this->prop['groups']['name_attr'];
@@ -1841,6 +1814,40 @@
        $this->cache->set('groups', $groups);
        return $groups;
    }
    /**
     * Fetch a group entry from LDAP and save in local cache
     */
    private function get_group_entry($group_id)
    {
        if (($group_cache = $this->cache->get('groups')) === null)
            $group_cache = $this->_fetch_groups();
        // add group record to cache if it isn't yet there
        if (!isset($group_cache[$group_id])) {
            $name_attr = $this->prop['groups']['name_attr'];
            $dn = self::dn_decode($group_id);
            $this->_debug("C: Read Group [dn: $dn]");
            if ($result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr))) {
                $list = ldap_get_entries($this->conn, $result);
                $entry = $list[0];
                $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
                $group_cache[$group_id]['ID'] = $group_id;
                $group_cache[$group_id]['dn'] = $dn;
                $group_cache[$group_id]['name'] = $group_name;
                $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
            }
            else {
                $this->_debug("S: ".ldap_error($this->conn));
                $group_cache[$group_id] = false;
            }
            $this->cache->set('groups', $group_cache);
        }
        return $group_cache[$group_id];
    }
    /**
@@ -2246,6 +2253,59 @@
    }
    /**
     * Wrapper for ldap_search or ldap_list depending on the given scope.
     * It optionally uses VLV index if configured in $prop
     *
     * @param string Base DN to read from
     * @param string Query filter to use
     * @param string Listing scope (sub|list|base)
     * @param array List of entry attributes to read
     * @param array Hash array with query configuration properties:
     *   - vlv: true if VLV index should be used
     *   - sort: array of sort attributes (has to be in sync with the VLV index)
     */
    protected function ldap_search($base_dn, $filter = '(objectclass=*)', $scope = 'sub', $attrs = array('dn'), $prop = array())
    {
        if ($this->ready) {
            $this->_debug("C: LDAP Search [$filter][dn: $base_dn]");
            // set VLV controls if requested
            if ($prop['vlv'] && $scope != 'base')
                $this->vlv_active = $this->_vlv_set_controls($prop, $this->list_page, $this->page_size);
            $function = $this->_scope2func($scope);
            if ($ldap_result = $function($this->conn, $base_dn, $filter,
                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
            ) {
                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
                    if (ldap_parse_result($this->conn, $this->ldap_result,
                        $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
                        && $serverctrls // can be null e.g. in case of adm. limit error
                    ) {
                        ldap_parse_virtuallist_control($this->conn, $serverctrls,
                            $last_offset, $this->vlv_count, $vresult);
                        $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
                    }
                    else {
                        $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
                    }
                }
                $entries_count = ldap_count_entries($this->conn, $ldap_result);
                $this->_debug("S: $entries_count record(s)");
                return $ldap_result;
            }
            else {
                $this->_debug("S: ".ldap_error($this->conn));
            }
        }
        return false;
    }
    /**
     * Wrapper for ldap_add()
     */
    protected function ldap_add($dn, $entry)