From 2f8b1036da42ec3d15a51c6b17a473f9f4df71d3 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli <bruederli@kolabsys.com> Date: Sat, 07 Feb 2015 12:33:24 -0500 Subject: [PATCH] Bump version and copyright year --- program/lib/Roundcube/rcube_ldap.php | 289 +++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 195 insertions(+), 94 deletions(-) diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index 522c81d..13d55bd 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -27,15 +27,16 @@ */ class rcube_ldap extends rcube_addressbook { - /** public properties */ + // public properties public $primary_key = 'ID'; public $groups = false; public $readonly = true; public $ready = false; public $group_id = 0; public $coltypes = array(); + public $export_groups = false; - /** private properties */ + // private properties protected $ldap; protected $prop = array(); protected $fieldmap = array(); @@ -46,9 +47,25 @@ protected $mail_domain = ''; protected $debug = false; + /** + * Group objectclass (lowercase) to member attribute mapping + * + * @var array + */ + private $group_types = array( + 'group' => 'member', + 'groupofnames' => 'member', + 'kolabgroupofnames' => 'member', + 'groupofuniquenames' => 'uniqueMember', + 'kolabgroupofuniquenames' => 'uniqueMember', + 'univentiongroup' => 'uniqueMember', + 'groupofurls' => null, + ); + private $base_dn = ''; private $groups_base_dn = ''; - private $group_url; + private $group_data; + private $group_search_cache; private $cache; @@ -78,12 +95,15 @@ $this->prop['groups']['name_attr'] = 'cn'; if (empty($this->prop['groups']['scope'])) $this->prop['groups']['scope'] = 'sub'; + // extend group objectclass => member attribute mapping + if (!empty($this->prop['groups']['class_member_attr'])) + $this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']); // add group name attrib to the list of attributes to be fetched $fetch_attributes[] = $this->prop['groups']['name_attr']; } - if (is_array($p['group_filters']) && count($p['group_filters'])) { - $this->groups = true; + if (is_array($p['group_filters'])) { + $this->groups = $this->groups || count($p['group_filters']); foreach ($p['group_filters'] as $k => $group_filter) { // set default name attribute to cn @@ -97,13 +117,14 @@ // fieldmap property is given if (is_array($p['fieldmap'])) { + $p['fieldmap'] = array_filter($p['fieldmap']); foreach ($p['fieldmap'] as $rf => $lf) $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf)); } else if (!empty($p)) { // read deprecated *_field properties to remain backwards compatible foreach ($p as $prop => $value) - if (preg_match('/^(.+)_field$/', $prop, $matches)) + if (!empty($value) && preg_match('/^(.+)_field$/', $prop, $matches)) $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value)); } @@ -213,8 +234,7 @@ // initialize ldap wrapper object $this->ldap = new rcube_ldap_generic($this->prop); - $this->ldap->set_cache($this->cache); - $this->ldap->set_debug($this->debug); + $this->ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); $this->_connect(); } @@ -273,7 +293,17 @@ $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); // Search for the dn to use to authenticate - if ($this->prop['search_base_dn'] && $this->prop['search_filter']) { + if ($this->prop['search_base_dn'] && $this->prop['search_filter'] + && (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn')) + ) { + $search_attribs = array('uid'); + if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) { + foreach ($search_bind_attrib as $r => $attr) { + $search_attribs[] = $attr; + $replaces[$r] = ''; + } + } + $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces); $search_base_dn = strtr($this->prop['search_base_dn'], $replaces); $search_filter = strtr($this->prop['search_filter'], $replaces); @@ -291,8 +321,7 @@ // we need to use a separate LDAP connection if (!empty($this->prop['vlv'])) { $ldap = new rcube_ldap_generic($this->prop); - $ldap->set_debug($this->debug); - $ldap->set_cache($this->cache); + $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); if (!$ldap->connect($host)) { continue; } @@ -303,10 +332,18 @@ } } - $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid')); + $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs); if ($res) { $res->rewind(); - $replaces['%dn'] = $res->get_dn(); + $replaces['%dn'] = key($res->entries(TRUE)); + + // add more replacements from 'search_bind_attrib' config + if ($search_bind_attrib) { + $res = $res->current(); + foreach ($search_bind_attrib as $r => $attr) { + $replaces[$r] = $res[$attr][0]; + } + } } if ($ldap != $this->ldap) { @@ -336,6 +373,24 @@ $bind_dn = strtr($bind_dn, $replaces); $this->base_dn = strtr($this->base_dn, $replaces); $this->groups_base_dn = strtr($this->groups_base_dn, $replaces); + + // replace placeholders in filter settings + if (!empty($this->prop['filter'])) + $this->prop['filter'] = strtr($this->prop['filter'], $replaces); + + foreach (array('base_dn','filter','member_filter') as $k) { + if (!empty($this->prop['groups'][$k])) + $this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces); + } + + if (is_array($this->prop['group_filters'])) { + foreach ($this->prop['group_filters'] as $i => $gf) { + if (!empty($gf['base_dn'])) + $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces); + if (!empty($gf['filter'])) + $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces); + } + } if (empty($bind_user)) { $bind_user = $u; @@ -499,7 +554,8 @@ $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size); } else { - $prop = $this->group_id ? $this->group_data : $this->prop; + $prop = $this->group_id ? $this->group_data : $this->prop; + $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn; // use global search filter if (!empty($this->filter)) @@ -507,7 +563,7 @@ // exec LDAP search if no result resource is stored if ($this->ready && !$this->ldap_result) - $this->ldap_result = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop); + $this->ldap_result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop); // count contacts for this user $this->result = $this->count(); @@ -540,9 +596,10 @@ /** * Get all members of the given group * - * @param string Group DN - * @param array Group entries (if called recursively) - * @return array Accumulated group members + * @param string Group DN + * @param boolean Count only + * @param array Group entries (if called recursively) + * @return array Accumulated group members */ function list_group_members($dn, $count = false, $entries = null) { @@ -550,7 +607,7 @@ // fetch group object if (empty($entries)) { - $attribs = array('dn','objectClass','member','uniqueMember','memberURL'); + $attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types)); $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs); if ($entries === false) { return $group_members; @@ -562,16 +619,16 @@ $attrs = array(); foreach ((array)$entry['objectclass'] as $objectclass) { - if (strtolower($objectclass) == 'groupofurls') { - $members = $this->_list_group_memberurl($dn, $entry, $count); - $group_members = array_merge($group_members, $members); - } - else if (($member_attr = $this->get_group_member_attr(array($objectclass), '')) + if (($member_attr = $this->get_group_member_attr(array($objectclass), '')) && ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs) ) { $members = $this->_list_group_members($dn, $entry, $member_attr, $count); $group_members = array_merge($group_members, $members); $attrs[] = $member_attr; + } + else if (!empty($entry['memberurl'])) { + $members = $this->_list_group_memberurl($dn, $entry, $count); + $group_members = array_merge($group_members, $members); } if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) { @@ -589,6 +646,7 @@ * @param string Group DN * @param array Group entry * @param string Member attribute to use + * @param boolean Count only * @return array Accumulated group members */ private function _list_group_members($dn, $entry, $attr, $count) @@ -602,8 +660,7 @@ // read these attributes for all members $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes']; - $attrib[] = 'member'; - $attrib[] = 'uniqueMember'; + $attrib = array_merge($attrib, array_values($this->group_types)); $attrib[] = 'memberURL'; $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)'; @@ -650,7 +707,7 @@ if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) { $entries = $result->entries(); for ($j = 0; $j < $entries['count']; $j++) { - if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))) + if ($this->is_group_entry($entries[$j]) && ($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]; @@ -709,16 +766,16 @@ if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) { $this->result = new rcube_result_set(0); - $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : ''; + $this->ldap->config_set('fuzzy_search', intval($this->prop['fuzzy_search'] && $mode != 1)); $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'], - array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */)); + array('search' => $value /*, 'sort' => $this->prop['sort'] */)); if ($ldap_data === false) { return $this->result; } // get all entries of this page and post-filter those that really match the query $search = mb_strtolower($value); - foreach ($ldap_data as $i => $entry) { + foreach ($ldap_data as $entry) { $rec = $this->_ldap2result($entry); foreach ($fields as $f) { foreach ((array)$rec[$f] as $val) { @@ -826,13 +883,13 @@ } // We have a connection but no result set, attempt to get one. else if ($this->ready) { - $prop = $this->group_id ? $this->group_data : $this->prop; + $prop = $this->group_id ? $this->group_data : $this->prop; + $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn; if (!empty($this->filter)) { // Use global search filter $prop['filter'] = $this->filter; } - - $count = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], array('dn'), $prop, true); + $count = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], array('dn'), $prop, true); } return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); @@ -848,7 +905,6 @@ { return $this->result; } - /** * Get a specific contact record @@ -889,6 +945,23 @@ } return $assoc ? $res : $this->result; + } + + /** + * Returns the last error occurred (e.g. when updating/inserting failed) + * + * @return array Hash array with the following fields: type, message + */ + function get_error() + { + $err = $this->error; + + // check ldap connection for errors + if (!$err && $this->ldap->get_error()) { + $err = array(self::ERROR_SEARCH, $this->ldap->get_error()); + } + + return $err; } @@ -1009,7 +1082,7 @@ } } - if (!$this->ldap->add($dn, $newentry)) { + if (!$this->ldap->add_entry($dn, $newentry)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1021,7 +1094,7 @@ 'objectClass' => (array) $this->prop['sub_fields'][$xidx], ); - $this->ldap->add($xdn, $xf); + $this->ldap->add_entry($xdn, $xf); } $dn = self::dn_encode($dn); @@ -1164,7 +1237,7 @@ if (!empty($subdeldata)) { foreach ($subdeldata as $fld => $val) { $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; - if (!$this->ldap->delete($subdn)) { + if (!$this->ldap->delete_entry($subdn)) { return false; } } @@ -1191,8 +1264,7 @@ // change the group membership of the contact if ($this->groups) { $group_ids = $this->get_record_groups($dn); - foreach ($group_ids as $group_id => $group_prop) - { + foreach (array_keys($group_ids) as $group_id) { $this->remove_from_group($group_id, $dn); $this->add_to_group($group_id, $newdn); } @@ -1209,7 +1281,7 @@ $fld => $val, 'objectClass' => (array) $this->prop['sub_fields'][$fld], ); - $this->ldap->add($subdn, $xf); + $this->ldap->add_entry($subdn, $xf); } } @@ -1239,7 +1311,7 @@ if ($this->sub_filter) { if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) { foreach ($entries as $entry) { - if (!$this->ldap->delete($entry['dn'])) { + if (!$this->ldap->delete_entry($entry['dn'])) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1248,16 +1320,16 @@ } // Delete the record. - if (!$this->ldap->delete($dn)) { + if (!$this->ldap->delete_entry($dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } - // remove contact from all groups where he was member + // remove contact from all groups where he was a member if ($this->groups) { $dn = self::dn_encode($dn); $group_ids = $this->get_record_groups($dn); - foreach ($group_ids as $group_id => $group_prop) { + foreach (array_keys($group_ids) as $group_id) { $this->remove_from_group($group_id, $dn); } } @@ -1269,8 +1341,10 @@ /** * Remove all contact records + * + * @param bool $with_groups Delete also groups if enabled */ - function delete_all() + function delete_all($with_groups = false) { // searching for contact entries $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)'); @@ -1280,6 +1354,16 @@ $dn_list[$idx] = self::dn_encode($entry['dn']); } $this->delete($dn_list); + } + + if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) { + foreach ($groups as $group) { + $this->ldap->delete_entry($group['dn']); + } + + if ($this->cache) { + $this->cache->remove('groups'); + } } } @@ -1336,10 +1420,20 @@ $out[$this->primary_key] = self::dn_encode($rec['dn']); // determine record type - if (self::is_group_entry($rec)) { + if ($this->is_group_entry($rec)) { $out['_type'] = 'group'; $out['readonly'] = true; $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr']; + } + + // assign object type from object class mapping + if (!empty($this->prop['class_type_map'])) { + foreach (array_map('strtolower', (array)$rec['objectclass']) as $objcls) { + if (!empty($this->prop['class_type_map'][$objcls])) { + $out['_type'] = $this->prop['class_type_map'][$objcls]; + break; + } + } } foreach ($fieldmap as $rf => $lf) @@ -1437,7 +1531,6 @@ return $ldap_data; } - /** * Returns unified attribute name (resolving aliases) */ @@ -1445,11 +1538,11 @@ { // list of known attribute aliases static $aliases = array( - 'gn' => 'givenname', + 'gn' => 'givenname', 'rfc822mailbox' => 'email', - 'userid' => 'uid', - 'emailaddress' => 'email', - 'pkcs9email' => 'email', + 'userid' => 'uid', + 'emailaddress' => 'email', + 'pkcs9email' => 'email', ); list($name, $limit) = explode(':', $namev, 2); @@ -1461,25 +1554,12 @@ /** * Determines whether the given LDAP entry is a group record */ - private static function is_group_entry($entry) + private function is_group_entry($entry) { - return array_intersect( - array('group', 'groupofnames', 'kolabgroupofnames', 'groupofuniquenames', - 'kolabgroupofuniquenames', 'groupofurls', 'univentiongroup'), - array_map('strtolower', (array)$entry['objectclass']) - ); - } + $classes = array_map('strtolower', (array)$entry['objectclass']); - /** - * Prints debug info to the log - */ - private function _debug($str) - { - if ($this->debug) { - rcube::write_log('ldap', $str); - } + return count(array_intersect(array_keys($this->group_types), $classes)) > 0; } - /** * Activate/deactivate debug mode @@ -1491,10 +1571,9 @@ $this->debug = $dbg; if ($this->ldap) { - $this->ldap->set_debug($dbg); + $this->ldap->config_set('debug', $dbg); } } - /** * Setter for the current group @@ -1528,12 +1607,12 @@ return array(); } - $group_cache = $this->_fetch_groups(); + $group_cache = $this->_fetch_groups($search, $mode); $groups = array(); if ($search) { foreach ($group_cache as $group) { - if ($this->compare_search_value('name', $group['name'], $search, $mode)) { + if ($this->compare_search_value('name', $group['name'], mb_strtolower($search), $mode)) { $groups[] = $group; } } @@ -1548,16 +1627,26 @@ /** * Fetch groups from server */ - private function _fetch_groups($vlv_page = null) + private function _fetch_groups($search = null, $mode = 0, $vlv_page = null) { + // reset group search cache + if ($search !== null && $vlv_page === null) { + $this->group_search_cache = null; + } + // return in-memory cache from previous search results + else if (is_array($this->group_search_cache) && $vlv_page === null) { + return $this->group_search_cache; + } + // special case: list groups from 'group_filters' config - if ($vlv_page === null && !empty($this->prop['group_filters'])) { + if ($vlv_page === null && $search === null && is_array($this->prop['group_filters'])) { $groups = array(); + $rcube = rcube::get_instance(); // list regular groups configuration as special filter if (!empty($this->prop['groups']['filter'])) { $id = '__groups__'; - $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups']; + $groups[$id] = array('ID' => $id, 'name' => $rcube->gettext('groups'), 'virtual' => true) + $this->prop['groups']; } foreach ($this->prop['group_filters'] as $id => $prop) { @@ -1567,12 +1656,13 @@ return $groups; } - if ($this->cache && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) { + if ($this->cache && $search === null && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) { return $groups; } $base_dn = $this->groups_base_dn; $filter = $this->prop['groups']['filter']; + $scope = $this->prop['groups']['scope']; $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); @@ -1588,12 +1678,26 @@ } $ldap = clone $this->ldap; - $ldap->set_config($this->prop['groups']); + $ldap->config_set($this->prop['groups']); $ldap->set_vlv_page($vlv_page+1, $page_size); } - $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)); - $ldap_data = $ldap->search($base_dn, $filter, $this->prop['groups']['scope'], $attrs, $this->prop['groups']); + $props = array('sort' => $this->prop['groups']['sort']); + $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)); + + // add search filter + if ($search !== null) { + // set wildcards + $wp = $ws = ''; + if (!empty($this->prop['fuzzy_search']) && $mode != 1) { + $ws = '*'; + $wp = !$mode ? '*' : ''; + } + $filter = "(&$filter($name_attr=$wp" . rcube_ldap_generic::quote_string($search) . "$ws))"; + $props['search'] = $wp . $search . $ws; + } + + $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $props); if ($ldap_data === false) { return array(); @@ -1630,7 +1734,7 @@ // call recursively until we have fetched all groups while ($this->prop['groups']['vlv'] && $group_count == $page_size) { - $next_page = $this->_fetch_groups(++$vlv_page); + $next_page = $this->_fetch_groups($search, $mode, ++$vlv_page); $groups = array_merge($groups, $next_page); $group_count = count($next_page); } @@ -1641,8 +1745,11 @@ } // cache this - if ($this->cache) { + if ($this->cache && $search === null) { $this->cache->set('groups', $groups); + } + else if ($search !== null) { + $this->group_search_cache = $groups; } return $groups; @@ -1712,7 +1819,7 @@ $member_attr => '', ); - if (!$this->ldap->add($new_dn, $new_entry)) { + if (!$this->ldap->add_entry($new_dn, $new_entry)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1735,7 +1842,7 @@ $group_cache = $this->_fetch_groups(); $del_dn = $group_cache[$group_id]['dn']; - if (!$this->ldap->delete($del_dn)) { + if (!$this->ldap->delete_entry($del_dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1864,12 +1971,13 @@ $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; $member_attr = $this->get_group_member_attr(); $add_filter = ''; + if ($member_attr != 'member' && $member_attr != 'uniqueMember') $add_filter = "($member_attr=$contact_dn)"; $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\')); $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr)); - if ($res === false) { + if ($ldap_data === false) { return array(); } @@ -1879,8 +1987,9 @@ $entry['dn'] = $ldap_data->get_dn(); $group_name = $entry[$name_attr][0]; $group_id = self::dn_encode($entry['dn']); - $groups[$group_id] = array('ID' => $group_id, 'name' => $group_name, 'dn' => $entry['dn']); + $groups[$group_id] = $group_name; } + return $groups; } @@ -1895,16 +2004,8 @@ if (!empty($object_classes)) { foreach ((array)$object_classes as $oc) { - switch (strtolower($oc)) { - case 'group': - case 'groupofnames': - case 'kolabgroupofnames': - return 'member'; - - case 'groupofuniquenames': - case 'kolabgroupofuniquenames': - case 'univentiongroup': - return 'uniqueMember'; + if ($attr = $this->group_types[strtolower($oc)]) { + return $attr; } } } -- Gitblit v1.9.1