From 24f1bf0f91254443ad848686b7afdb1a785e79e8 Mon Sep 17 00:00:00 2001 From: thomascube <thomas@roundcube.net> Date: Wed, 02 May 2012 17:25:47 -0400 Subject: [PATCH] Fix handling of 'serialzied' LDAP address attributes --- program/include/rcube_ldap.php | 202 ++++++++++++++++++++++++++++++++++---------------- 1 files changed, 138 insertions(+), 64 deletions(-) diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php index 9f8086b..17fb407 100644 --- a/program/include/rcube_ldap.php +++ b/program/include/rcube_ldap.php @@ -63,12 +63,11 @@ /** * Object constructor * - * @param array LDAP connection properties - * @param boolean Enables debug mode - * @param string Current user mail domain name - * @param integer User-ID + * @param array $p LDAP connection properties + * @param boolean $debug Enables debug mode + * @param string $mail_domain Current user mail domain name */ - function __construct($p, $debug=false, $mail_domain=NULL) + function __construct($p, $debug = false, $mail_domain = null) { $this->prop = $p; @@ -103,23 +102,39 @@ } // use fieldmap to advertise supported coltypes to the application - foreach ($this->fieldmap as $col => $lf) { - list($col, $type) = explode(':', $col); + foreach ($this->fieldmap as $colv => $lfv) { + list($col, $type) = explode(':', $colv); + list($lf, $limit, $delim) = explode(':', $lfv); + + if ($limit == '*') $limit = null; + else $limit = max(1, intval($limit)); + if (!is_array($this->coltypes[$col])) { $subtypes = $type ? array($type) : null; - $this->coltypes[$col] = array('limit' => 1, 'subtypes' => $subtypes); + $this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes); } elseif ($type) { $this->coltypes[$col]['subtypes'][] = $type; - $this->coltypes[$col]['limit']++; + $this->coltypes[$col]['limit'] += $limit; } + + if ($delim) + $this->coltypes[$col]['serialized'][$type] = $delim; + if ($type && !$this->fieldmap[$col]) - $this->fieldmap[$col] = $lf; + $this->fieldmap[$col] = $lf; + + $this->fieldmap[$colv] = $lf; } // support for composite address if ($this->fieldmap['street'] && $this->fieldmap['locality']) { - $this->coltypes['address'] = array('limit' => max(1, $this->coltypes['locality']['limit']), 'subtypes' => $this->coltypes['locality']['subtypes'], 'childs' => array()); + $this->coltypes['address'] = array( + 'limit' => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']), + 'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], $this->coltypes['locality']['subtypes']), + 'childs' => array(), + ) + (array)$this->coltypes['address']; + foreach (array('street','locality','zipcode','region','country') as $childcol) { if ($this->fieldmap[$childcol]) { $this->coltypes['address']['childs'][$childcol] = array('type' => 'text'); @@ -128,7 +143,14 @@ } } else if ($this->coltypes['address']) { - $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40); + $this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40); + + // 'serialized' means the UI has to present a composite address field + if ($this->coltypes['address']['serialized']) { + $childprop = array('type' => 'text'); + $this->coltypes['address']['type'] = 'composite'; + $this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop); + } } // make sure 'required_fields' is an array @@ -164,8 +186,8 @@ $this->mail_domain = $mail_domain; // initialize cache - $rcmail = rcmail::get_instance(); - $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600); + $rcube = rcube::get_instance(); + $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600); $this->_connect(); } @@ -176,10 +198,10 @@ */ private function _connect() { - global $RCMAIL; + $rcube = rcube::get_instance(); if (!function_exists('ldap_connect')) - raise_error(array('code' => 100, 'type' => 'ldap', + rcube::raise_error(array('code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "No ldap support in this installation of PHP"), true, true); @@ -195,7 +217,7 @@ foreach ($this->prop['hosts'] as $host) { - $host = idn_to_ascii(rcube_parse_host($host)); + $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : ''); $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]"); @@ -225,7 +247,7 @@ } if (!is_resource($this->conn)) { - raise_error(array('code' => 100, 'type' => 'ldap', + rcube::raise_error(array('code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not connect to any LDAP server, last tried $hostname"), true); @@ -244,11 +266,11 @@ if ($this->prop['user_specific']) { // No password set, use the session password if (empty($bind_pass)) { - $bind_pass = $RCMAIL->decrypt($_SESSION['password']); + $bind_pass = $rcube->decrypt($_SESSION['password']); } // Get the pieces needed for variable replacement. - if ($fu = $RCMAIL->user->get_username()) + if ($fu = $rcube->get_user_name()) list($u, $d) = explode('@', $fu); else $d = $this->mail_domain; @@ -287,7 +309,7 @@ if (!empty($this->prop['search_dn_default'])) $replaces['%dn'] = $this->prop['search_dn_default']; else { - raise_error(array( + rcube::raise_error(array( 'code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "DN not found using LDAP search."), true); @@ -341,7 +363,7 @@ } if (!function_exists('ldap_sasl_bind')) { - raise_error(array('code' => 100, 'type' => 'ldap', + rcube::raise_error(array('code' => 100, 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Unable to bind: ldap_sasl_bind() not exists"), true, true); @@ -367,7 +389,7 @@ $this->_debug("S: ".ldap_error($this->conn)); - raise_error(array( + rcube::raise_error(array( 'code' => ldap_errno($this->conn), 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)), @@ -400,7 +422,7 @@ $this->_debug("S: ".ldap_error($this->conn)); - raise_error(array( + rcube::raise_error(array( 'code' => ldap_errno($this->conn), 'type' => 'ldap', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)), @@ -993,10 +1015,13 @@ if ($missing) { // try to complete record automatically if ($autofix) { - $reverse_map = array_flip($this->fieldmap); - $name_parts = preg_split('/[\s,.]+/', $save_data['name']); $sn_field = $this->fieldmap['surname']; $fn_field = $this->fieldmap['firstname']; + $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]) { $save_data['surname'] = array_pop($name_parts); @@ -1006,6 +1031,16 @@ if ($fn_field && $missing[$fn_field]) { $save_data['firstname'] = array_shift($name_parts); unset($missing[$fn_field]); + } + + // try to fix missing e-mail, very often on import + // from vCard we have email:other only defined + if ($mail_field && $missing[$mail_field]) { + $emails = $this->get_col_values('email', $save_data, true); + if (!empty($emails) && ($email = array_shift($emails))) { + $save_data['email'] = $email; + unset($missing[$mail_field]); + } } } @@ -1109,6 +1144,14 @@ $ldap_data = $this->_map_data($save_cols); $old_data = $record['_raw_attrib']; + // special handling of photo col + if ($photo_fld = $this->fieldmap['photo']) { + // undefined means keep old photo + if (!array_key_exists('photo', $save_cols)) { + $ldap_data[$photo_fld] = $record['photo']; + } + } + foreach ($this->fieldmap as $col => $fld) { if ($fld) { $val = $ldap_data[$fld]; @@ -1120,6 +1163,9 @@ // make sure comparing array with one element with a string works as expected if (is_array($old) && count($old) == 1 && !is_array($val)) { $old = array_pop($old); + } + if (is_array($val) && count($val) == 1 && !is_array($old)) { + $val = array_pop($val); } // Subentries must be handled separately if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) { @@ -1136,6 +1182,7 @@ } continue; } + // The field does exist compare it to the ldap record. if ($old != $val) { // Changed, but find out how. @@ -1157,18 +1204,9 @@ } // end if } // end if } // end foreach -/* - console($old_data); - console($ldap_data); - console('----'); - console($newdata); - console($replacedata); - console($deletedata); - console('----'); - console($subdata); - console($subnewdata); - console($subdeldata); -*/ + + // console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata); + $dn = self::dn_decode($id); // Update the entry as required. @@ -1456,6 +1494,8 @@ $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain); else if (in_array($col, array('street','zipcode','locality','country','region'))) $out['address'.($subtype?':':'').$subtype][$i][$col] = $value; + else if ($col == 'address' && strpos($value, '$') !== false) // address data is represented as string separated with $ + list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $value); else if ($rec[$lf]['count'] > 1) $out[$rf][] = $value; else @@ -1498,6 +1538,15 @@ } } } + + // if addresses are to be saved as serialized string, do so + 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'])); + } + } } $ldap_data = array(); @@ -1518,17 +1567,21 @@ /** * Returns unified attribute name (resolving aliases) */ - private static function _attr_name($name) + private static function _attr_name($namev) { // list of known attribute aliases - $aliases = array( + static $aliases = array( 'gn' => 'givenname', 'rfc822mailbox' => 'email', 'userid' => 'uid', 'emailaddress' => 'email', 'pkcs9email' => 'email', ); - return isset($aliases[$name]) ? $aliases[$name] : $name; + + list($name, $limit) = explode(':', $namev, 2); + $suffix = $limit ? ':'.$limit : ''; + + return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix; } @@ -1537,8 +1590,9 @@ */ private function _debug($str) { - if ($this->debug) - write_log('ldap', $str); + if ($this->debug) { + rcube::write_log('ldap', $str); + } } @@ -1673,23 +1727,7 @@ $groups[$group_id]['ID'] = $group_id; $groups[$group_id]['dn'] = $ldap_data[$i]['dn']; $groups[$group_id]['name'] = $group_name; - $groups[$group_id]['member_attr'] = $this->prop['member_attr']; - - // check objectClass attributes of group and act accordingly - for ($j=0; $j < $ldap_data[$i]['objectclass']['count']; $j++) { - switch (strtolower($ldap_data[$i]['objectclass'][$j])) { - case 'group': - case 'groupofnames': - case 'kolabgroupofnames': - $groups[$group_id]['member_attr'] = 'member'; - break; - - case 'groupofuniquenames': - case 'kolabgroupofuniquenames': - $groups[$group_id]['member_attr'] = 'uniqueMember'; - break; - } - } + $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']); // list email attributes of a group for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) { @@ -1750,8 +1788,8 @@ $base_dn = $this->groups_base_dn; $new_dn = "cn=$group_name,$base_dn"; $new_gid = self::dn_encode($group_name); - $member_attr = $this->prop['groups']['member_attr']; - $name_attr = $this->prop['groups']['name_attr']; + $member_attr = $this->get_group_member_attr(); + $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; $new_entry = array( 'objectClass' => $this->prop['groups']['object_classes'], @@ -1903,8 +1941,8 @@ $base_dn = $this->groups_base_dn; $contact_dn = self::dn_decode($contact_id); - $name_attr = $this->prop['groups']['name_attr']; - $member_attr = $this->prop['member_attr']; + $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)"; @@ -1931,6 +1969,42 @@ return $groups; } + /** + * Detects group member attribute name + */ + private function get_group_member_attr($object_classes = array()) + { + 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 (!empty($member_attr)) { + return $member_attr; + } + + if (!empty($this->prop['groups']['member_attr'])) { + return $this->prop['groups']['member_attr']; + } + + return 'member'; + } + /** * Generate BER encoded string for Virtual List View option -- Gitblit v1.9.1