Aleksander Machniak
2013-10-17 197203727417a03d87053a47e5aa5175a76e3e0b
program/include/rcube_ldap.php
@@ -35,8 +35,6 @@
    public $readonly = true;
    public $ready = false;
    public $group_id = 0;
    public $list_page = 1;
    public $page_size = 10;
    public $coltypes = array();
    /** private properties */
@@ -47,7 +45,6 @@
    protected $filter = '';
    protected $result = null;
    protected $ldap_result = null;
    protected $sort_col = '';
    protected $mail_domain = '';
    protected $debug = false;
@@ -103,24 +100,53 @@
        }
        // 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' => 2, 'subtypes' => $subtypes);
                $this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf));
            }
            elseif ($type) {
                $this->coltypes[$col]['subtypes'][] = $type;
                $this->coltypes[$col]['limit']++;
                $this->coltypes[$col]['attributes'][] = $lf;
                $this->coltypes[$col]['limit'] += $limit;
            }
            if ($type && !$this->fieldmap[$col])
                $this->fieldmap[$col] = $lf;
            if ($delim)
                $this->coltypes[$col]['serialized'][$type] = $delim;
            $this->fieldmap[$colv] = $lf;
        }
        if ($this->fieldmap['street'] && $this->fieldmap['locality'])
            $this->coltypes['address'] = array('limit' => 1);
        else if ($this->coltypes['address'])
            $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
        if ($this->coltypes['street'] && $this->coltypes['locality']) {
            $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->coltypes[$childcol]) {
                    $this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
                    unset($this->coltypes[$childcol]);  // remove address child col from global coltypes list
                }
            }
        }
        else if ($this->coltypes['address']) {
            $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
        if (!is_array($this->prop['required_fields']))
@@ -181,6 +207,9 @@
                ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
                $this->prop['host'] = $host;
                $this->conn = $lc;
                if (isset($this->prop['referrals']))
                    ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
                break;
            }
            $this->_debug("S: NOT OK");
@@ -225,6 +254,10 @@
            $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'])) {
                    $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
                }
                // 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);
@@ -399,24 +432,15 @@
    /**
     * Set internal list page
     * Set internal sort settings
     *
     * @param number $page Page number to list
     * @param string $sort_col Sort column
     * @param string $sort_order Sort order
     */
    function set_page($page)
    function set_sort_order($sort_col, $sort_order = null)
    {
        $this->list_page = (int)$page;
    }
    /**
     * Set internal page size
     *
     * @param number $size Number of messages to display on one page
     */
    function set_pagesize($size)
    {
        $this->page_size = (int)$size;
        if ($this->coltypes[$sort_col]['attributes'])
            $this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
    }
@@ -526,11 +550,11 @@
    }
    /**
    * Get all members of the given group
    *
    * @param string Group DN
    * @param array  Group entries (if called recursively)
    * @return array Accumulated group members
     * Get all members of the given group
     *
     * @param string Group DN
     * @param array  Group entries (if called recursively)
     * @return array Accumulated group members
     */
    function list_group_members($dn, $count = false, $entries = null)
    {
@@ -604,6 +628,9 @@
        for ($i=0; $i < $entry[$attr]['count']; $i++)
        {
            if (empty($entry[$attr][$i]))
                continue;
            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
@@ -648,14 +675,11 @@
            $attrib = $count ? array('dn') : array_values($this->fieldmap);
            if ($result = @$func($this->conn, $m[1], $filter,
                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
            {
                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
            ) {
                $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
                if ($err = ldap_errno($this->conn))
                    $this->_debug("S: Error: " .ldap_err2str($err));
            }
            else
            {
            else {
                $this->_debug("S: ".ldap_error($this->conn));
                return $group_members;
            }
@@ -687,15 +711,20 @@
     *
     * @param mixed   $fields   The field name of array of field names to search in
     * @param mixed   $value    Search value (or array of values when $fields is array)
     * @param boolean $strict   True for strict, False for partial (fuzzy) matching
     * @param int     $mode     Matching mode:
     *                          0 - partial (*abc*),
     *                          1 - strict (=),
     *                          2 - prefix (abc*)
     * @param boolean $select   True if results are requested, False if count only
     * @param boolean $nocount  (Not used)
     * @param array   $required List of fields that cannot be empty
     *
     * @return array  Indexed list of contact records and 'count' value
     */
    function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
    function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
    {
        $mode = intval($mode);
        // special treatment for ID-based search
        if ($fields == 'ID' || $fields == $this->primary_key)
        {
@@ -724,16 +753,44 @@
            $function = $this->_scope2func($this->prop['scope']);
            $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
                array_values($this->fieldmap), 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
                array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
            $this->result = new rcube_result_set(0);
            if (!$this->ldap_result) {
                $this->_debug("S: ".ldap_error($this->conn));
                return $this->result;
            }
            $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
            // get all entries of this page and post-filter those that really match the query
            $this->result = new rcube_result_set(0);
            $search = mb_strtolower($value);
            $entries = ldap_get_entries($this->conn, $this->ldap_result);
            for ($i = 0; $i < $entries['count']; $i++) {
                $rec = $this->_ldap2result($entries[$i]);
                if (stripos($rec['name'] . $rec['email'], $value) !== false) {
                    $this->result->add($rec);
                    $this->result->count++;
                foreach ($fields as $f) {
                    foreach ((array)$rec[$f] as $val) {
                        $val = mb_strtolower($val);
                        switch ($mode) {
                        case 1:
                            $got = ($val == $search);
                            break;
                        case 2:
                            $got = ($search == substr($val, 0, strlen($search)));
                            break;
                        default:
                            $got = (strpos($val, $search) !== false);
                            break;
                        }
                        if ($got) {
                            $this->result->add($rec);
                            $this->result->count++;
                            break 2;
                        }
                    }
                }
            }
@@ -742,7 +799,14 @@
        // use AND operator for advanced searches
        $filter = is_array($value) ? '(&' : '(|';
        $wc     = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
        // set wildcards
        $wp = $ws = '';
        if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
            $ws = '*';
            if (!$mode) {
                $wp = '*';
            }
        }
        if ($fields == '*')
        {
@@ -756,7 +820,7 @@
            if (is_array($this->prop['search_fields']))
            {
                foreach ($this->prop['search_fields'] as $field) {
                    $filter .= "($field=$wc" . $this->_quote_string($value) . "$wc)";
                    $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
                }
            }
        }
@@ -764,8 +828,13 @@
        {
            foreach ((array)$fields as $idx => $field) {
                $val = is_array($value) ? $value[$idx] : $value;
                if ($f = $this->_map_field($field)) {
                    $filter .= "($f=$wc" . $this->_quote_string($val) . "$wc)";
                if ($attrs = $this->_map_field($field)) {
                    if (count($attrs) > 1)
                        $filter .= '(|';
                    foreach ($attrs as $f)
                        $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
                    if (count($attrs) > 1)
                        $filter .= ')';
                }
            }
        }
@@ -773,9 +842,16 @@
        // add required (non empty) fields filter
        $req_filter = '';
        foreach ((array)$required as $field)
            if ($f = $this->_map_field($field))
                $req_filter .= "($f=*)";
        foreach ((array)$required as $field) {
            if ($attrs = $this->_map_field($field)) {
                if (count($attrs) > 1)
                    $req_filter .= '(|';
                foreach ($attrs as $f)
                    $req_filter .= "($f=*)";
                if (count($attrs) > 1)
                    $req_filter .= ')';
            }
        }
        if (!empty($req_filter))
            $filter = '(&' . $req_filter . $filter . ')';
@@ -977,7 +1053,7 @@
        $dn = self::dn_encode($dn);
        // add new contact to the selected group
        if ($this->groups)
        if ($this->group_id)
            $this->add_to_group($this->group_id, $dn);
        return $dn;
@@ -1002,39 +1078,33 @@
        $replacedata = array();
        $deletedata = array();
        // flatten composite fields in $record
        if (is_array($record['address'])) {
          foreach ($record['address'] as $i => $struct) {
            foreach ($struct as $col => $val) {
              $record[$col][$i] = $val;
            }
          }
        }
        $ldap_data = $this->_map_data($save_cols);
        $old_data = $record['_raw_attrib'];
        foreach ($this->fieldmap as $col => $fld) {
            $val = $save_cols[$col];
            $val = $ldap_data[$fld];
            if ($fld) {
                // remove empty array values
                if (is_array($val))
                    $val = array_filter($val);
                // The field does exist compare it to the ldap record.
                if ($record[$col] != $val) {
                if ($old_data[$fld] != $val) {
                    // Changed, but find out how.
                    if (!isset($record[$col])) {
                    if (!isset($old_data[$fld])) {
                        // Field was not set prior, need to add it.
                        $newdata[$fld] = $val;
                    } // end if
                    elseif ($val == '') {
                    }
                    else if ($val == '') {
                        // Field supplied is empty, verify that it is not required.
                        if (!in_array($fld, $this->prop['required_fields'])) {
                            // It is not, safe to clear.
                            $deletedata[$fld] = $record[$col];
                        } // end if
                            $deletedata[$fld] = $old_data[$fld];
                        }
                    } // end elseif
                    else {
                        // The data was modified, save it out.
                        $replacedata[$fld] = $val;
                    } // end else
                    }
                } // end if
            } // end if
        } // end foreach
@@ -1188,15 +1258,14 @@
            // only fetch dn for count (should keep the payload low)
            $attrs = $count ? array('dn') : array_values($this->fieldmap);
            if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
            {
                $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
                if ($err = ldap_errno($this->conn))
                    $this->_debug("S: Error: " .ldap_err2str($err));
                return $count ? ldap_count_entries($this->conn, $this->ldap_result) : true;
                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
            ) {
                $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
                $this->_debug("S: $entries_count record(s)");
                return $count ? $entries_count : true;
            }
            else
            {
            else {
                $this->_debug("S: ".ldap_error($this->conn));
            }
        }
@@ -1262,10 +1331,16 @@
            for ($i=0; $i < $rec[$lf]['count']; $i++) {
                if (!($value = $rec[$lf][$i]))
                    continue;
                if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
                list($col, $subtype) = explode(':', $rf);
                $out['_raw_attrib'][$lf][$i] = $value;
                if ($col == 'email' && $this->mail_domain && !strpos($value, '@'))
                    $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
                else if (in_array($rf, array('street','zipcode','locality','country','region')))
                    $out['address'][$i][$rf] = $value;
                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
@@ -1274,7 +1349,7 @@
            // Make sure name fields aren't arrays (#1488108)
            if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
                $out[$rf] = $out[$rf][0];
                $out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
            }
        }
@@ -1283,11 +1358,11 @@
    /**
     * Return real field name (from fields map)
     * Return LDAP attribute(s) for the given field
     */
    private function _map_field($field)
    {
        return $this->fieldmap[$field];
        return (array)$this->coltypes[$field]['attributes'];
    }
@@ -1296,9 +1371,43 @@
     */
    private function _map_data($save_cols)
    {
        // flatten composite fields first
        foreach ($this->coltypes as $col => $colprop) {
            if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
                foreach ($values as $subtype => $childs) {
                    $subtype = $subtype ? ':'.$subtype : '';
                    foreach ($childs as $i => $child_values) {
                        foreach ((array)$child_values as $childcol => $value) {
                            $save_cols[$childcol.$subtype][$i] = $value;
                        }
                    }
                }
            }
            // 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();
        foreach ($this->fieldmap as $col => $fld) {
            $val = $save_cols[$col];
        foreach ($this->fieldmap as $rf => $fld) {
            $val = $save_cols[$rf];
            // check for value in base field (eg.g email instead of email:foo)
            list($col, $subtype) = explode(':', $rf);
            if (!$val && !empty($save_cols[$col])) {
                $val = $save_cols[$col];
                unset($save_cols[$col]);  // only use this value once
            }
            else if (!$val && !$subtype) { // extract values from subtype cols
                $val = $this->get_col_values($col, $save_cols, true);
            }
            if (is_array($val))
                $val = array_filter($val);  // remove empty entries
            if ($fld && $val) {
@@ -1306,7 +1415,7 @@
                $ldap_data[$fld] = $val;
            }
        }
        return $ldap_data;
    }
@@ -1314,17 +1423,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;
    }
@@ -1335,6 +1448,18 @@
    {
        if ($this->debug)
            write_log('ldap', $str);
    }
    /**
     * Activate/deactivate debug mode
     *
     * @param boolean $dbg True if LDAP commands should be logged
     * @access public
     */
    function set_debug($dbg = true)
    {
        $this->debug = $dbg;
    }
@@ -1402,9 +1527,9 @@
        $groups = array();
        if ($search) {
            $search = strtolower($search);
            $search = mb_strtolower($search);
            foreach ($group_cache as $group) {
                if (strstr(strtolower($group['name']), $search))
                if (strpos(mb_strtolower($group['name']), $search) !== false)
                    $groups[] = $group;
            }
        }
@@ -1480,7 +1605,7 @@
                    $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
            }
            $group_sortnames[] = strtolower($ldap_data[$i][$sort_attr][0]);
            $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
        }
        // recursive call can exit here
@@ -1533,11 +1658,13 @@
        $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'];
        $new_entry = array(
            'objectClass' => $this->prop['groups']['object_classes'],
            $name_attr => $group_name,
            $member_attr => '',
        );
        $this->_debug("C: Add [dn: $new_dn]: ".print_r($new_entry, true));
@@ -1634,13 +1761,16 @@
        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'];
        $member_attr = $group_cache[$group_id]['member_attr'];
        $group_dn    = "cn=$group_name,$base_dn";
        $new_attrs = array();
        foreach (explode(",", $contact_ids) as $id)
        foreach ($contact_ids as $id)
            $new_attrs[$member_attr][] = self::dn_decode($id);
        $this->_debug("C: Add [dn: $group_dn]: ".print_r($new_attrs, true));