From 18b40c1a3214518764e99f69b581bd7c90426091 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Wed, 04 Dec 2013 07:58:43 -0500 Subject: [PATCH] Fix issue where groups were not deleted when "Replace entire addressbook" option on contacts import was used (#1489420) --- program/lib/Roundcube/rcube_ldap.php | 340 ++++++++++++++++++++++++++++++++----------------------- 1 files changed, 197 insertions(+), 143 deletions(-) diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index 54077c6..2d4aa08 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -27,28 +27,44 @@ */ 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 $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 $prop = array(); protected $fieldmap = array(); + protected $filter = ''; protected $sub_filter; - protected $filter = ''; - protected $result = null; - protected $ldap_result = null; + protected $result; + protected $ldap_result; protected $mail_domain = ''; protected $debug = false; - private $base_dn = ''; + /** + * 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 = null; + private $group_url; private $cache; @@ -65,9 +81,6 @@ $fetch_attributes = array('objectClass'); - if (isset($p['searchonly'])) - $this->searchonly = $p['searchonly']; - // check if groups are configured if (is_array($p['groups']) && count($p['groups'])) { $this->groups = true; @@ -81,6 +94,9 @@ $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']; @@ -88,7 +104,11 @@ if (is_array($p['group_filters']) && count($p['group_filters'])) { $this->groups = true; - foreach ($p['group_filters'] as $group_filter) { + foreach ($p['group_filters'] as $k => $group_filter) { + // set default name attribute to cn + if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr'])) + $this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn'; + if ($group_filter['name_attr']) $fetch_attributes[] = $group_filter['name_attr']; } @@ -272,7 +292,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); @@ -302,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(); + + // 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) { @@ -335,6 +373,23 @@ $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); + if (!empty($this->prop['groups']['filter'])) + $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces); + if (!empty($this->prop['groups']['member_filter'])) + $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces); + + if (!empty($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; @@ -498,7 +553,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 ? $this->group_base_dn : $this->base_dn; // use global search filter if (!empty($this->filter)) @@ -506,7 +562,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(); @@ -539,9 +595,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) { @@ -549,7 +606,8 @@ // fetch group object if (empty($entries)) { - $entries = $this->ldap->read_entries($dn, '(objectClass=*)', 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; } @@ -557,29 +615,25 @@ for ($i=0; $i < $entries['count']; $i++) { $entry = $entries[$i]; - - if (empty($entry['objectclass'])) - continue; + $attrs = array(); foreach ((array)$entry['objectclass'] as $objectclass) { - switch (strtolower($objectclass)) { - case "group": - 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 (($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']) { + break 2; } } - - if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) - break; } return array_filter($group_members); @@ -591,6 +645,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) @@ -598,13 +653,13 @@ // 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])) + if (empty($entry[$attr])) { return $group_members; + } // 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=*)'; @@ -651,7 +706,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]; @@ -827,13 +882,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); @@ -1192,8 +1247,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); } @@ -1258,7 +1312,7 @@ 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); } } @@ -1270,8 +1324,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=*)'); @@ -1281,6 +1337,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($group['dn']); + } + + if ($this->cache) { + $this->cache->remove('groups'); + } } } @@ -1337,7 +1403,7 @@ $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']; @@ -1446,11 +1512,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); @@ -1462,12 +1528,11 @@ /** * 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'), - array_map('strtolower', (array)$entry['objectclass']) - ); + $classes = array_map('strtolower', (array)$entry['objectclass']); + + return count(array_intersect(array_keys($this->group_types), $classes)) > 0; } /** @@ -1524,15 +1589,13 @@ */ function list_groups($search = null, $mode = 0) { - if (!$this->groups) + if (!$this->groups) { return array(); - - // use cached list for searching - if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { - $group_cache = $this->_fetch_groups(); } - $groups = array(); + $group_cache = $this->_fetch_groups(); + $groups = array(); + if ($search) { foreach ($group_cache as $group) { if ($this->compare_search_value('name', $group['name'], $search, $mode)) { @@ -1540,8 +1603,9 @@ } } } - else + else { $groups = $group_cache; + } return array_values($groups); } @@ -1549,10 +1613,10 @@ /** * Fetch groups from server */ - private function _fetch_groups($vlv_page = 0) + private function _fetch_groups($vlv_page = null) { // special case: list groups from 'group_filters' config - if (!empty($this->prop['group_filters'])) { + if ($vlv_page === null && !empty($this->prop['group_filters'])) { $groups = array(); // list regular groups configuration as special filter @@ -1568,35 +1632,43 @@ return $groups; } - $base_dn = $this->groups_base_dn; - $filter = $this->prop['groups']['filter']; - $name_attr = $this->prop['groups']['name_attr']; + if ($this->cache && $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); - $sort_attr = $sort_attrs[0]; + $sort_attr = $sort_attrs[0]; $ldap = $this->ldap; // use vlv to list groups if ($this->prop['groups']['vlv']) { $page_size = 200; - if (!$this->prop['groups']['sort']) + if (!$this->prop['groups']['sort']) { $this->prop['groups']['sort'] = $sort_attrs; + } $ldap = clone $this->ldap; $ldap->set_config($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']); + $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)); + $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $this->prop['groups']); + if ($ldap_data === false) { return array(); } - $groups = array(); + $groups = array(); $group_sortnames = array(); - $group_count = $ldap_data->count(); + $group_count = $ldap_data->count(); + foreach ($ldap_data as $entry) { if (!$entry['dn']) // DN is mandatory $entry['dn'] = $ldap_data->get_dn(); @@ -1618,19 +1690,21 @@ } // recursive call can exit here - if ($vlv_page > 0) + if ($vlv_page > 0) { return $groups; + } // call recursively until we have fetched all groups while ($this->prop['groups']['vlv'] && $group_count == $page_size) { - $next_page = $this->_fetch_groups(++$vlv_page); - $groups = array_merge($groups, $next_page); + $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']) + if (!$this->prop['groups']['vlv']) { array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups); + } // cache this if ($this->cache) { @@ -1645,9 +1719,7 @@ */ private function get_group_entry($group_id) { - if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { - $group_cache = $this->_fetch_groups(); - } + $group_cache = $this->_fetch_groups(); // add group record to cache if it isn't yet there if (!isset($group_cache[$group_id])) { @@ -1696,12 +1768,11 @@ */ function create_group($group_name) { - $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn; - $new_gid = self::dn_encode($new_dn); + $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn; + $new_gid = self::dn_encode($new_dn); $member_attr = $this->get_group_member_attr(); - $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; - - $new_entry = array( + $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; + $new_entry = array( 'objectClass' => $this->prop['groups']['object_classes'], $name_attr => $group_name, $member_attr => '', @@ -1727,11 +1798,8 @@ */ function delete_group($group_id) { - if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { - $group_cache = $this->_fetch_groups(); - } - - $del_dn = $group_cache[$group_id]['dn']; + $group_cache = $this->_fetch_groups(); + $del_dn = $group_cache[$group_id]['dn']; if (!$this->ldap->delete($del_dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); @@ -1756,13 +1824,10 @@ */ function rename_group($group_id, $new_name, &$new_gid) { - if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { - $group_cache = $this->_fetch_groups(); - } - - $old_dn = $group_cache[$group_id]['dn']; - $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true); - $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn); + $group_cache = $this->_fetch_groups(); + $old_dn = $group_cache[$group_id]['dn']; + $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true); + $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn); if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); @@ -1786,19 +1851,18 @@ */ function add_to_group($group_id, $contact_ids) { - if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { - $group_cache = $this->_fetch_groups(); - } - - if (!is_array($contact_ids)) - $contact_ids = explode(',', $contact_ids); - + $group_cache = $this->_fetch_groups(); $member_attr = $group_cache[$group_id]['member_attr']; $group_dn = $group_cache[$group_id]['dn']; $new_attrs = array(); - foreach ($contact_ids as $id) + if (!is_array($contact_ids)) { + $contact_ids = explode(',', $contact_ids); + } + + foreach ($contact_ids as $id) { $new_attrs[$member_attr][] = self::dn_decode($id); + } if (!$this->ldap->mod_add($group_dn, $new_attrs)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); @@ -1822,19 +1886,18 @@ */ function remove_from_group($group_id, $contact_ids) { - if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { - $group_cache = $this->_fetch_groups(); - } - - if (!is_array($contact_ids)) - $contact_ids = explode(',', $contact_ids); - + $group_cache = $this->_fetch_groups(); $member_attr = $group_cache[$group_id]['member_attr']; $group_dn = $group_cache[$group_id]['dn']; - $del_attrs = array(); + $del_attrs = array(); - foreach ($contact_ids as $id) + if (!is_array($contact_ids)) { + $contact_ids = explode(',', $contact_ids); + } + + foreach ($contact_ids as $id) { $del_attrs[$member_attr][] = self::dn_decode($id); + } if (!$this->ldap->mod_del($group_dn, $del_attrs)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); @@ -1858,14 +1921,16 @@ */ function get_record_groups($contact_id) { - if (!$this->groups) + if (!$this->groups) { return array(); + } $base_dn = $this->groups_base_dn; $contact_dn = self::dn_decode($contact_id); $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('\\' => '\\\\')); @@ -1881,45 +1946,34 @@ $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; } /** * Detects group member attribute name */ - private function get_group_member_attr($object_classes = array()) + private function get_group_member_attr($object_classes = array(), $default = 'member') { if (empty($object_classes)) { $object_classes = $this->prop['groups']['object_classes']; } + if (!empty($object_classes)) { foreach ((array)$object_classes as $oc) { - switch (strtolower($oc)) { - case 'group': - case 'groupofnames': - case 'kolabgroupofnames': - $member_attr = 'member'; - break; - - case 'groupofuniquenames': - case 'kolabgroupofuniquenames': - $member_attr = 'uniqueMember'; - break; + if ($attr = $this->group_types[strtolower($oc)]) { + return $attr; } } - } - - if (!empty($member_attr)) { - return $member_attr; } if (!empty($this->prop['groups']['member_attr'])) { return $this->prop['groups']['member_attr']; } - return 'member'; + return $default; } -- Gitblit v1.9.1