From d93019125cca783e8acfaa68467024375321e55f Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Tue, 28 Oct 2014 07:20:00 -0400 Subject: [PATCH] Allways call rcube_imap::set_folder() before rcube_imap::get_message_part() --- program/lib/Roundcube/rcube_ldap.php | 683 +++++++++++++++++++++++++++++++++++--------------------- 1 files changed, 427 insertions(+), 256 deletions(-) diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index c117b79..6805c49 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -27,35 +27,53 @@ */ 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 $fieldmap = array('_objclass' => 'objectclass'); + 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 $group_data; + private $group_search_cache; private $cache; /** * Object constructor * - * @param array $p LDAP connection properties + * @param array $p LDAP connection properties * @param boolean $debug Enables debug mode * @param string $mail_domain Current user mail domain name */ @@ -63,8 +81,7 @@ { $this->prop = $p; - if (isset($p['searchonly'])) - $this->searchonly = $p['searchonly']; + $fetch_attributes = array('objectClass'); // check if groups are configured if (is_array($p['groups']) && count($p['groups'])) { @@ -79,12 +96,24 @@ $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 fieldmap in order to have it fetched - $this->fieldmap['_groupname'] = $this->prop['groups']['name_attr']; + // add group name attrib to the list of attributes to be fetched + $fetch_attributes[] = $this->prop['groups']['name_attr']; } - else 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 + 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']; + } } // fieldmap property is given @@ -172,7 +201,7 @@ // Build sub_fields filter if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) { $this->sub_filter = ''; - foreach ($this->prop['sub_fields'] as $attr => $class) { + foreach ($this->prop['sub_fields'] as $class) { if (!empty($class)) { $class = is_array($class) ? array_pop($class) : $class; $this->sub_filter .= '(objectClass=' . $class . ')'; @@ -189,11 +218,23 @@ // initialize cache $rcube = rcube::get_instance(); - $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600); + if ($cache_type = $rcube->config->get('ldap_cache', 'db')) { + $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m'); + $cache_name = 'LDAP.' . asciiwords($this->prop['name']); + + $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl); + } + + // determine which attributes to fetch + $this->prop['list_attributes'] = array_unique($fetch_attributes); + $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes); + foreach ($rcube->config->get('contactlist_fields') as $col) { + $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col)); + } // initialize ldap wrapper object - $this->prop['attributes'] = array_values($this->fieldmap); - $this->ldap = new rcube_ldap_generic($this->prop, true); + $this->ldap = new rcube_ldap_generic($this->prop); + $this->ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); $this->_connect(); } @@ -251,31 +292,63 @@ $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u); - if ($this->prop['search_base_dn'] && $this->prop['search_filter']) { - if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) { - if (!$this->ldap->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw'])) - continue; // bind failed, try neyt host - } + // Search for the dn to use to authenticate + 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 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); + $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); - $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}"); + $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:" + .$this->prop['search_bind_pw']); - // TODO: use $this->ldap->search() here - $res = @ldap_search($this->ldap->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid')); - if ($res) { - if (($entry = ldap_first_entry($this->ldap->conn, $res)) - && ($bind_dn = ldap_get_dn($this->ldap->conn, $entry)) - ) { - $this->_debug("S: search returned dn: $bind_dn"); - $dn = ldap_explode_dn($bind_dn, 1); - $replaces['%dn'] = $dn[0]; - } + if ($this->cache && ($dn = $this->cache->get($cache_key))) { + $replaces['%dn'] = $dn; } else { - $this->_debug("S: ".ldap_error($this->ldap->conn)); + $ldap = $this->ldap; + if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) { + // To protect from "Critical extension is unavailable" error + // we need to use a separate LDAP connection + if (!empty($this->prop['vlv'])) { + $ldap = new rcube_ldap_generic($this->prop); + $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug)); + if (!$ldap->connect($host)) { + continue; + } + } + + if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) { + continue; // bind failed, try next host + } + } + + $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs); + if ($res) { + $res->rewind(); + $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) { + $ldap->close(); + } } // DN not found @@ -287,8 +360,12 @@ 'code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "DN not found using LDAP search."), true); - return false; + continue; } + } + + if ($this->cache && !empty($replaces['%dn'])) { + $this->cache->set($cache_key, $replaces['%dn']); } } @@ -296,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; @@ -459,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)) @@ -467,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(); @@ -500,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) { @@ -510,8 +607,8 @@ // fetch group object if (empty($entries)) { - $this->_debug("C: Read Group [dn: $dn]"); - $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; } @@ -519,29 +616,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); @@ -553,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) @@ -560,14 +654,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') : array_values($this->fieldmap); - $attrib[] = 'objectClass'; - $attrib[] = 'member'; - $attrib[] = 'uniqueMember'; + $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes']; + $attrib = array_merge($attrib, array_values($this->group_types)); $attrib[] = 'memberURL'; $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)'; @@ -603,19 +696,18 @@ { $group_members = array(); - for ($i=0; $i < $entry['memberurl']['count']; $i++) - { + for ($i=0; $i < $entry['memberurl']['count']; $i++) { // extract components from url if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m)) continue; // add search filter if any $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3]; - $attrs = $count ? array('dn') : $this->prop['attributes']; + $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes']; 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 ($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]; @@ -672,18 +764,19 @@ $list_fields = $rcube->config->get('contactlist_fields'); if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) { - $entries = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'], - array('vlv' => true, 'search' => $value, 'sort' => $this->prop['sort'])); $this->result = new rcube_result_set(0); - if ($entries === false) { + $search_suffix = $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'] */)); + 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); - for ($i = 0; $i < $entries['count']; $i++) { - $rec = $this->_ldap2result($entries[$i]); + $search = mb_strtolower($value); + foreach ($ldap_data as $i => $entry) { + $rec = $this->_ldap2result($entry); foreach ($fields as $f) { foreach ((array)$rec[$f] as $val) { if ($this->compare_search_value($f, $val, $search, $mode)) { @@ -718,7 +811,7 @@ } if (is_array($this->prop['search_fields'])) { foreach ($this->prop['search_fields'] as $field) { - $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)"; + $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)"; } } } @@ -729,7 +822,7 @@ if (count($attrs) > 1) $filter .= '(|'; foreach ($attrs as $f) - $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)"; + $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)"; if (count($attrs) > 1) $filter .= ')'; } @@ -740,6 +833,8 @@ // add required (non empty) fields filter $req_filter = ''; foreach ((array)$required as $field) { + if (in_array($field, (array)$fields)) // required field is already in search filter + continue; if ($attrs = $this->_map_field($field)) { if (count($attrs) > 1) $req_filter .= '(|'; @@ -788,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); @@ -810,7 +905,6 @@ { return $this->result; } - /** * Get a specific contact record @@ -853,6 +947,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; + } + /** * Check the given data before saving. @@ -892,7 +1003,6 @@ $mail_field = $this->fieldmap['email']; // try to extract surname and firstname from displayname - $reverse_map = array_flip($this->fieldmap); $name_parts = preg_split('/[\s,.]+/', $save_data['name']); if ($sn_field && $missing[$sn_field]) { @@ -959,12 +1069,12 @@ } // Build the new entries DN. - $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn; + $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn; // Remove attributes that need to be added separately (child objects) $xfields = array(); if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) { - foreach ($this->prop['sub_fields'] as $xf => $xclass) { + foreach (array_keys($this->prop['sub_fields']) as $xf) { if (!empty($newentry[$xf])) { $xfields[$xf] = $newentry[$xf]; unset($newentry[$xf]); @@ -972,19 +1082,19 @@ } } - if (!$this->ldap->add($dn, $newentry)) { + if (!$this->ldap->add_entry($dn, $newentry)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } foreach ($xfields as $xidx => $xf) { - $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn; + $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn; $xf = array( $xidx => $xf, 'objectClass' => (array) $this->prop['sub_fields'][$xidx], ); - $this->ldap->add($xdn, $xf); + $this->ldap->add_entry($xdn, $xf); } $dn = self::dn_encode($dn); @@ -1027,7 +1137,7 @@ } } - foreach ($this->fieldmap as $col => $fld) { + foreach ($this->fieldmap as $fld) { if ($fld) { $val = $ldap_data[$fld]; $old = $old_data[$fld]; @@ -1100,11 +1210,11 @@ // Handle RDN change if ($replacedata[$this->prop['LDAP_rdn']]) { $newdn = $this->prop['LDAP_rdn'].'=' - .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true) + .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true) .','.$this->base_dn; if ($dn != $newdn) { $newrdn = $this->prop['LDAP_rdn'].'=' - .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true); + .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true); unset($replacedata[$this->prop['LDAP_rdn']]); } } @@ -1126,8 +1236,8 @@ // remove sub-entries if (!empty($subdeldata)) { foreach ($subdeldata as $fld => $val) { - $subdn = $fld.'='.$this->_quote_string($val).','.$dn; - if (!$this->ldap->delete($subdn)) { + $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; + if (!$this->ldap->delete_entry($subdn)) { return false; } } @@ -1154,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); } @@ -1167,12 +1276,12 @@ // add sub-entries if (!empty($subnewdata)) { foreach ($subnewdata as $fld => $val) { - $subdn = $fld.'='.$this->_quote_string($val).','.$dn; + $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; $xf = array( $fld => $val, 'objectClass' => (array) $this->prop['sub_fields'][$fld], ); - $this->ldap->add($subdn, $xf); + $this->ldap->add_entry($subdn, $xf); } } @@ -1202,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; } @@ -1211,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); } } @@ -1232,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=*)'); @@ -1244,6 +1355,16 @@ } $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'); + } + } } /** @@ -1253,6 +1374,10 @@ */ protected function add_autovalues(&$attrs) { + if (empty($this->prop['autovalues'])) { + return; + } + $attrvals = array(); foreach ($attrs as $k => $v) { $attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v; @@ -1260,13 +1385,24 @@ foreach ((array)$this->prop['autovalues'] as $lf => $templ) { if (empty($attrs[$lf])) { - // replace {attr} placeholders with concrete attribute values - $templ = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals)); + if (strpos($templ, '(') !== false) { + // replace {attr} placeholders with (escaped!) attribute values to be safely eval'd + $code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals))); + $fn = create_function('', "return ($code);"); + if (!$fn) { + rcube::raise_error(array( + 'code' => 505, 'type' => 'php', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Expression parse error on: ($code)"), true, false); + continue; + } - if (strpos($templ, '(') !== false) - $attrs[$lf] = eval("return ($templ);"); - else - $attrs[$lf] = $templ; + $attrs[$lf] = $fn(); + } + else { + // replace {attr} placeholders with concrete attribute values + $attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals)); + } } } } @@ -1284,10 +1420,20 @@ $out[$this->primary_key] = self::dn_encode($rec['dn']); // determine record type - if (array_intersect(array('groupofuniquenames','kolabgroupofuniquenames'), array_map('strtolower', (array)$rec['objectclass']))) { + if ($this->is_group_entry($rec)) { $out['_type'] = 'group'; $out['readonly'] = true; - $fieldmap['name'] = $fieldmap['_groupname']; + $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) @@ -1352,8 +1498,10 @@ if (is_array($colprop['serialized'])) { foreach ($colprop['serialized'] as $subtype => $delim) { $key = $col.':'.$subtype; - foreach ((array)$save_cols[$key] as $i => $val) - $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country'])); + foreach ((array)$save_cols[$key] as $i => $val) { + $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']); + $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null; + } } } } @@ -1391,11 +1539,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); @@ -1404,6 +1552,15 @@ return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix; } + /** + * Determines whether the given LDAP entry is a group record + */ + private function is_group_entry($entry) + { + $classes = array_map('strtolower', (array)$entry['objectclass']); + + return count(array_intersect(array_keys($this->group_types), $classes)) > 0; + } /** * Prints debug info to the log @@ -1420,36 +1577,14 @@ * Activate/deactivate debug mode * * @param boolean $dbg True if LDAP commands should be logged - * @access public */ function set_debug($dbg = true) { $this->debug = $dbg; - } - - /** - * Quotes attribute value string - * - * @param string $str Attribute value - * @param bool $dn True if the attribute is a DN - * - * @return string Quoted string - */ - private static function _quote_string($str, $dn=false) - { - // take firt entry if array given - if (is_array($str)) - $str = reset($str); - - if ($dn) - $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c', - '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23'); - else - $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c', - '/'=>'\2f'); - - return strtr($str, $replace); + if ($this->ldap) { + $this->ldap->config_set('debug', $dbg); + } } @@ -1472,28 +1607,32 @@ * List all active contact groups of this source * * @param string Optional search string to match group name + * @param int Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * * @return array Indexed list of contact groups, each a hash array */ - function list_groups($search = null) + function list_groups($search = null, $mode = 0) { - if (!$this->groups) + 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(); + $group_cache = $this->_fetch_groups($search, $mode); + $groups = array(); - $groups = array(); if ($search) { - $search = mb_strtolower($search); foreach ($group_cache as $group) { - if (strpos(mb_strtolower($group['name']), $search) !== false) + if ($this->compare_search_value('name', $group['name'], mb_strtolower($search), $mode)) { $groups[] = $group; + } } } - else + else { $groups = $group_cache; + } return array_values($groups); } @@ -1501,16 +1640,26 @@ /** * Fetch groups from server */ - private function _fetch_groups($vlv_page = 0) + 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 (!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) { @@ -1520,35 +1669,57 @@ return $groups; } - $base_dn = $this->groups_base_dn; - $filter = $this->prop['groups']['filter']; - $name_attr = $this->prop['groups']['name_attr']; + 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); - $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->config_set($this->prop['groups']); $ldap->set_vlv_page($vlv_page+1, $page_size); } + $props = array('sort' => $this->prop['groups']['sort']); $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']); + + // 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(); } - $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(); @@ -1570,22 +1741,29 @@ } // 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($search, $mode, ++$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 - $this->cache->set('groups', $groups); + if ($this->cache && $search === null) { + $this->cache->set('groups', $groups); + } + else if ($search !== null) { + $this->group_search_cache = $groups; + } return $groups; } @@ -1595,15 +1773,13 @@ */ private function get_group_entry($group_id) { - if (($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])) { $name_attr = $this->prop['groups']['name_attr']; $dn = self::dn_decode($group_id); - $this->_debug("C: Read Group [dn: $dn]"); if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) { $entry = $list[0]; $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr]; @@ -1616,7 +1792,9 @@ $group_cache[$group_id] = false; } - $this->cache->set('groups', $group_cache); + if ($this->cache) { + $this->cache->set('groups', $group_cache); + } } return $group_cache[$group_id]; @@ -1644,24 +1822,24 @@ */ function create_group($group_name) { - $base_dn = $this->groups_base_dn; - $new_dn = "cn=$group_name,$base_dn"; - $new_gid = self::dn_encode($group_name); + $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 => '', ); - 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; } - $this->cache->remove('groups'); + if ($this->cache) { + $this->cache->remove('groups'); + } return array('id' => $new_gid, 'name' => $group_name); } @@ -1674,19 +1852,18 @@ */ function delete_group($group_id) { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); + $group_cache = $this->_fetch_groups(); + $del_dn = $group_cache[$group_id]['dn']; - $base_dn = $this->groups_base_dn; - $group_name = $group_cache[$group_id]['name']; - $del_dn = "cn=$group_name,$base_dn"; - - if (!$this->ldap->delete($del_dn)) { + if (!$this->ldap->delete_entry($del_dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } - $this->cache->remove('groups'); + if ($this->cache) { + unset($group_cache[$group_id]); + $this->cache->set('groups', $group_cache); + } return true; } @@ -1701,21 +1878,19 @@ */ function rename_group($group_id, $new_name, &$new_gid) { - 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']; - $old_dn = "cn=$group_name,$base_dn"; - $new_rdn = "cn=$new_name"; - $new_gid = self::dn_encode($new_name); + $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'); return false; } - $this->cache->remove('groups'); + if ($this->cache) { + $this->cache->remove('groups'); + } return $new_name; } @@ -1723,66 +1898,71 @@ /** * Add the given contact records the a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be added - * @return int Number of contacts added + * @param string Group identifier + * @param array|string List of contact identifiers to be added + * + * @return int Number of contacts added */ function add_to_group($group_id, $contact_ids) { - if (($group_cache = $this->cache->get('groups')) === null) - $group_cache = $this->_fetch_groups(); - - if (!is_array($contact_ids)) - $contact_ids = explode(',', $contact_ids); - - $base_dn = $this->groups_base_dn; - $group_name = $group_cache[$group_id]['name']; + $group_cache = $this->_fetch_groups(); $member_attr = $group_cache[$group_id]['member_attr']; - $group_dn = "cn=$group_name,$base_dn"; + $group_dn = $group_cache[$group_id]['dn']; + $new_attrs = array(); - $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'); return 0; } - $this->cache->remove('groups'); + if ($this->cache) { + $this->cache->remove('groups'); + } - return count($new_attrs['member']); + return count($new_attrs[$member_attr]); } /** * Remove the given contact records from a certain group * - * @param string Group identifier - * @param array List of contact identifiers to be removed - * @return int Number of deleted group members + * @param string Group identifier + * @param array|string List of contact identifiers to be removed + * + * @return int Number of deleted group members */ function remove_from_group($group_id, $contact_ids) { - 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']; + $group_cache = $this->_fetch_groups(); $member_attr = $group_cache[$group_id]['member_attr']; - $group_dn = "cn=$group_name,$base_dn"; + $group_dn = $group_cache[$group_id]['dn']; + $del_attrs = array(); - $del_attrs = array(); - foreach (explode(",", $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'); return 0; } - $this->cache->remove('groups'); + if ($this->cache) { + $this->cache->remove('groups'); + } - return count($del_attrs['member']); + return count($del_attrs[$member_attr]); } /** @@ -1795,14 +1975,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('\\' => '\\\\')); @@ -1818,45 +2000,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