source: subversion/trunk/roundcubemail/program/include/rcube_ldap.php @ 6091

Last change on this file since 6091 was 6091, checked in by alec, 14 months ago
  • Framework refactoring (I hope it's the last one): rcube,rcmail,rcube_ui -> rcube,rcmail,rcube_utils renamed main.inc into rcube_bc.inc
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 75.1 KB
Line 
1<?php
2/*
3 +-----------------------------------------------------------------------+
4 | program/include/rcube_ldap.php                                        |
5 |                                                                       |
6 | This file is part of the Roundcube Webmail client                     |
7 | Copyright (C) 2006-2012, The Roundcube Dev Team                       |
8 | Copyright (C) 2011, Kolab Systems AG                                  |
9 |                                                                       |
10 | Licensed under the GNU General Public License version 3 or            |
11 | any later version with exceptions for skins & plugins.                |
12 | See the README file for a full license statement.                     |
13 |                                                                       |
14 | PURPOSE:                                                              |
15 |   Interface to an LDAP address directory                              |
16 |                                                                       |
17 +-----------------------------------------------------------------------+
18 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
19 |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
20 |         Aleksander Machniak <machniak@kolabsys.com>                   |
21 +-----------------------------------------------------------------------+
22
23 $Id$
24
25*/
26
27
28/**
29 * Model class to access an LDAP address directory
30 *
31 * @package Addressbook
32 */
33class rcube_ldap extends rcube_addressbook
34{
35    /** public properties */
36    public $primary_key = 'ID';
37    public $groups = false;
38    public $readonly = true;
39    public $ready = false;
40    public $group_id = 0;
41    public $coltypes = array();
42
43    /** private properties */
44    protected $conn;
45    protected $prop = array();
46    protected $fieldmap = array();
47    protected $sub_filter;
48    protected $filter = '';
49    protected $result = null;
50    protected $ldap_result = null;
51    protected $mail_domain = '';
52    protected $debug = false;
53
54    private $base_dn = '';
55    private $groups_base_dn = '';
56    private $group_url = null;
57    private $cache;
58
59    private $vlv_active = false;
60    private $vlv_count = 0;
61
62
63    /**
64    * Object constructor
65    *
66    * @param array       $p            LDAP connection properties
67    * @param boolean $debug        Enables debug mode
68    * @param string  $mail_domain  Current user mail domain name
69    */
70    function __construct($p, $debug = false, $mail_domain = null)
71    {
72        $this->prop = $p;
73
74        if (isset($p['searchonly']))
75            $this->searchonly = $p['searchonly'];
76
77        // check if groups are configured
78        if (is_array($p['groups']) && count($p['groups'])) {
79            $this->groups = true;
80            // set member field
81            if (!empty($p['groups']['member_attr']))
82                $this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
83            else if (empty($p['member_attr']))
84                $this->prop['member_attr'] = 'member';
85            // set default name attribute to cn
86            if (empty($this->prop['groups']['name_attr']))
87                $this->prop['groups']['name_attr'] = 'cn';
88            if (empty($this->prop['groups']['scope']))
89                $this->prop['groups']['scope'] = 'sub';
90        }
91
92        // fieldmap property is given
93        if (is_array($p['fieldmap'])) {
94            foreach ($p['fieldmap'] as $rf => $lf)
95                $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
96        }
97        else if (!empty($p)) {
98            // read deprecated *_field properties to remain backwards compatible
99            foreach ($p as $prop => $value)
100                if (preg_match('/^(.+)_field$/', $prop, $matches))
101                    $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
102        }
103
104        // use fieldmap to advertise supported coltypes to the application
105        foreach ($this->fieldmap as $col => $lf) {
106            list($col, $type) = explode(':', $col);
107            if (!is_array($this->coltypes[$col])) {
108                $subtypes = $type ? array($type) : null;
109                $this->coltypes[$col] = array('limit' => 1, 'subtypes' => $subtypes);
110            }
111            elseif ($type) {
112                $this->coltypes[$col]['subtypes'][] = $type;
113                $this->coltypes[$col]['limit']++;
114            }
115            if ($type && !$this->fieldmap[$col])
116                $this->fieldmap[$col] = $lf;
117        }
118
119        // support for composite address
120        if ($this->fieldmap['street'] && $this->fieldmap['locality']) {
121            $this->coltypes['address'] = array('limit' => max(1, $this->coltypes['locality']['limit']), 'subtypes' => $this->coltypes['locality']['subtypes'], 'childs' => array());
122            foreach (array('street','locality','zipcode','region','country') as $childcol) {
123                if ($this->fieldmap[$childcol]) {
124                    $this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
125                    unset($this->coltypes[$childcol]);  // remove address child col from global coltypes list
126                }
127            }
128        }
129        else if ($this->coltypes['address']) {
130            $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
131        }
132
133        // make sure 'required_fields' is an array
134        if (!is_array($this->prop['required_fields'])) {
135            $this->prop['required_fields'] = (array) $this->prop['required_fields'];
136        }
137
138        // make sure LDAP_rdn field is required
139        if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])) {
140            $this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
141        }
142
143        foreach ($this->prop['required_fields'] as $key => $val) {
144            $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
145        }
146
147        // Build sub_fields filter
148        if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
149            $this->sub_filter = '';
150            foreach ($this->prop['sub_fields'] as $attr => $class) {
151                if (!empty($class)) {
152                    $class = is_array($class) ? array_pop($class) : $class;
153                    $this->sub_filter .= '(objectClass=' . $class . ')';
154                }
155            }
156            if (count($this->prop['sub_fields']) > 1) {
157                $this->sub_filter = '(|' . $this->sub_filter . ')';
158            }
159        }
160
161        $this->sort_col    = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
162        $this->debug       = $debug;
163        $this->mail_domain = $mail_domain;
164
165        // initialize cache
166        $rcmail = rcmail::get_instance();
167        $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
168
169        $this->_connect();
170    }
171
172
173    /**
174    * Establish a connection to the LDAP server
175    */
176    private function _connect()
177    {
178        $RCMAIL = rcmail::get_instance();
179
180        if (!function_exists('ldap_connect'))
181            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
182                'file' => __FILE__, 'line' => __LINE__,
183                'message' => "No ldap support in this installation of PHP"),
184                true, true);
185
186        if (is_resource($this->conn))
187            return true;
188
189        if (!is_array($this->prop['hosts']))
190            $this->prop['hosts'] = array($this->prop['hosts']);
191
192        if (empty($this->prop['ldap_version']))
193            $this->prop['ldap_version'] = 3;
194
195        foreach ($this->prop['hosts'] as $host)
196        {
197            $host     = idn_to_ascii(rcube_utils::parse_host($host));
198            $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
199
200            $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
201
202            if ($lc = @ldap_connect($host, $this->prop['port']))
203            {
204                if ($this->prop['use_tls'] === true)
205                    if (!ldap_start_tls($lc))
206                        continue;
207
208                $this->_debug("S: OK");
209
210                ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
211                $this->prop['host'] = $host;
212                $this->conn = $lc;
213
214                if (isset($this->prop['referrals']))
215                    ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
216                break;
217            }
218            $this->_debug("S: NOT OK");
219        }
220
221        // See if the directory is writeable.
222        if ($this->prop['writable']) {
223            $this->readonly = false;
224        }
225
226        if (!is_resource($this->conn)) {
227            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
228                'file' => __FILE__, 'line' => __LINE__,
229                'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
230
231            return false;
232        }
233
234        $bind_pass = $this->prop['bind_pass'];
235        $bind_user = $this->prop['bind_user'];
236        $bind_dn   = $this->prop['bind_dn'];
237
238        $this->base_dn        = $this->prop['base_dn'];
239        $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
240        $this->prop['groups']['base_dn'] : $this->base_dn;
241
242        // User specific access, generate the proper values to use.
243        if ($this->prop['user_specific']) {
244            // No password set, use the session password
245            if (empty($bind_pass)) {
246                $bind_pass = $RCMAIL->decrypt($_SESSION['password']);
247            }
248
249            // Get the pieces needed for variable replacement.
250            if ($fu = $RCMAIL->get_user_name())
251                list($u, $d) = explode('@', $fu);
252            else
253                $d = $this->mail_domain;
254
255            $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
256
257            $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
258
259            if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
260                if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
261                    $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
262                }
263
264                // Search for the dn to use to authenticate
265                $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
266                $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
267
268                $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
269
270                $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
271                if ($res) {
272                    if (($entry = ldap_first_entry($this->conn, $res))
273                        && ($bind_dn = ldap_get_dn($this->conn, $entry))
274                    ) {
275                        $this->_debug("S: search returned dn: $bind_dn");
276                        $dn = ldap_explode_dn($bind_dn, 1);
277                        $replaces['%dn'] = $dn[0];
278                    }
279                }
280                else {
281                    $this->_debug("S: ".ldap_error($this->conn));
282                }
283
284                // DN not found
285                if (empty($replaces['%dn'])) {
286                    if (!empty($this->prop['search_dn_default']))
287                        $replaces['%dn'] = $this->prop['search_dn_default'];
288                    else {
289                        rcube::raise_error(array(
290                            'code' => 100, 'type' => 'ldap',
291                            'file' => __FILE__, 'line' => __LINE__,
292                            'message' => "DN not found using LDAP search."), true);
293                        return false;
294                    }
295                }
296            }
297
298            // Replace the bind_dn and base_dn variables.
299            $bind_dn              = strtr($bind_dn, $replaces);
300            $this->base_dn        = strtr($this->base_dn, $replaces);
301            $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
302
303            if (empty($bind_user)) {
304                $bind_user = $u;
305            }
306        }
307
308        if (empty($bind_pass)) {
309            $this->ready = true;
310        }
311        else {
312            if (!empty($bind_dn)) {
313                $this->ready = $this->bind($bind_dn, $bind_pass);
314            }
315            else if (!empty($this->prop['auth_cid'])) {
316                $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
317            }
318            else {
319                $this->ready = $this->sasl_bind($bind_user, $bind_pass);
320            }
321        }
322
323        return $this->ready;
324    }
325
326
327    /**
328     * Bind connection with (SASL-) user and password
329     *
330     * @param string $authc Authentication user
331     * @param string $pass  Bind password
332     * @param string $authz Autorization user
333     *
334     * @return boolean True on success, False on error
335     */
336    public function sasl_bind($authc, $pass, $authz=null)
337    {
338        if (!$this->conn) {
339            return false;
340        }
341
342        if (!function_exists('ldap_sasl_bind')) {
343            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
344                'file' => __FILE__, 'line' => __LINE__,
345                'message' => "Unable to bind: ldap_sasl_bind() not exists"),
346                true, true);
347        }
348
349        if (!empty($authz)) {
350            $authz = 'u:' . $authz;
351        }
352
353        if (!empty($this->prop['auth_method'])) {
354            $method = $this->prop['auth_method'];
355        }
356        else {
357            $method = 'DIGEST-MD5';
358        }
359
360        $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
361
362        if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
363            $this->_debug("S: OK");
364            return true;
365        }
366
367        $this->_debug("S: ".ldap_error($this->conn));
368
369        rcube::raise_error(array(
370            'code' => ldap_errno($this->conn), 'type' => 'ldap',
371            'file' => __FILE__, 'line' => __LINE__,
372            'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
373            true);
374
375        return false;
376    }
377
378
379    /**
380     * Bind connection with DN and password
381     *
382     * @param string Bind DN
383     * @param string Bind password
384     *
385     * @return boolean True on success, False on error
386     */
387    public function bind($dn, $pass)
388    {
389        if (!$this->conn) {
390            return false;
391        }
392
393        $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
394
395        if (@ldap_bind($this->conn, $dn, $pass)) {
396            $this->_debug("S: OK");
397            return true;
398        }
399
400        $this->_debug("S: ".ldap_error($this->conn));
401
402        rcube::raise_error(array(
403            'code' => ldap_errno($this->conn), 'type' => 'ldap',
404            'file' => __FILE__, 'line' => __LINE__,
405            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
406            true);
407
408        return false;
409    }
410
411
412    /**
413     * Close connection to LDAP server
414     */
415    function close()
416    {
417        if ($this->conn)
418        {
419            $this->_debug("C: Close");
420            ldap_unbind($this->conn);
421            $this->conn = null;
422        }
423    }
424
425
426    /**
427     * Returns address book name
428     *
429     * @return string Address book name
430     */
431    function get_name()
432    {
433        return $this->prop['name'];
434    }
435
436
437    /**
438     * Set internal sort settings
439     *
440     * @param string $sort_col Sort column
441     * @param string $sort_order Sort order
442     */
443    function set_sort_order($sort_col, $sort_order = null)
444    {
445        if ($this->fieldmap[$sort_col])
446            $this->sort_col = $this->fieldmap[$sort_col];
447    }
448
449
450    /**
451     * Save a search string for future listings
452     *
453     * @param string $filter Filter string
454     */
455    function set_search_set($filter)
456    {
457        $this->filter = $filter;
458    }
459
460
461    /**
462     * Getter for saved search properties
463     *
464     * @return mixed Search properties used by this class
465     */
466    function get_search_set()
467    {
468        return $this->filter;
469    }
470
471
472    /**
473     * Reset all saved results and search parameters
474     */
475    function reset()
476    {
477        $this->result = null;
478        $this->ldap_result = null;
479        $this->filter = '';
480    }
481
482
483    /**
484     * List the current set of contact records
485     *
486     * @param  array  List of cols to show
487     * @param  int    Only return this number of records
488     *
489     * @return array  Indexed list of contact records, each a hash array
490     */
491    function list_records($cols=null, $subset=0)
492    {
493        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
494        {
495            $this->result = new rcube_result_set(0);
496            $this->result->searchonly = true;
497            return $this->result;
498        }
499
500        // fetch group members recursively
501        if ($this->group_id && $this->group_data['dn'])
502        {
503            $entries = $this->list_group_members($this->group_data['dn']);
504
505            // make list of entries unique and sort it
506            $seen = array();
507            foreach ($entries as $i => $rec) {
508                if ($seen[$rec['dn']]++)
509                    unset($entries[$i]);
510            }
511            usort($entries, array($this, '_entry_sort_cmp'));
512
513            $entries['count'] = count($entries);
514            $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
515        }
516        else
517        {
518            // add general filter to query
519            if (!empty($this->prop['filter']) && empty($this->filter))
520                $this->set_search_set($this->prop['filter']);
521
522            // exec LDAP search if no result resource is stored
523            if ($this->conn && !$this->ldap_result)
524                $this->_exec_search();
525
526            // count contacts for this user
527            $this->result = $this->count();
528
529            // we have a search result resource
530            if ($this->ldap_result && $this->result->count > 0)
531            {
532                // sorting still on the ldap server
533                if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
534                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
535
536                // get all entries from the ldap server
537                $entries = ldap_get_entries($this->conn, $this->ldap_result);
538            }
539
540        }  // end else
541
542        // start and end of the page
543        $start_row = $this->vlv_active ? 0 : $this->result->first;
544        $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
545        $last_row = $this->result->first + $this->page_size;
546        $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
547
548        // filter entries for this page
549        for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
550            $this->result->add($this->_ldap2result($entries[$i]));
551
552        return $this->result;
553    }
554
555    /**
556     * Get all members of the given group
557     *
558     * @param string Group DN
559     * @param array  Group entries (if called recursively)
560     * @return array Accumulated group members
561     */
562    function list_group_members($dn, $count = false, $entries = null)
563    {
564        $group_members = array();
565
566        // fetch group object
567        if (empty($entries)) {
568            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
569            if ($result === false)
570            {
571                $this->_debug("S: ".ldap_error($this->conn));
572                return $group_members;
573            }
574
575            $entries = @ldap_get_entries($this->conn, $result);
576        }
577
578        for ($i=0; $i < $entries['count']; $i++)
579        {
580            $entry = $entries[$i];
581
582            if (empty($entry['objectclass']))
583                continue;
584
585            foreach ((array)$entry['objectclass'] as $objectclass)
586            {
587                switch (strtolower($objectclass)) {
588                    case "group":
589                    case "groupofnames":
590                    case "kolabgroupofnames":
591                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
592                        break;
593                    case "groupofuniquenames":
594                    case "kolabgroupofuniquenames":
595                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
596                        break;
597                    case "groupofurls":
598                        $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
599                        break;
600                }
601            }
602
603            if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
604              break;
605        }
606
607        return array_filter($group_members);
608    }
609
610    /**
611     * Fetch members of the given group entry from server
612     *
613     * @param string Group DN
614     * @param array  Group entry
615     * @param string Member attribute to use
616     * @return array Accumulated group members
617     */
618    private function _list_group_members($dn, $entry, $attr, $count)
619    {
620        // Use the member attributes to return an array of member ldap objects
621        // NOTE that the member attribute is supposed to contain a DN
622        $group_members = array();
623        if (empty($entry[$attr]))
624            return $group_members;
625
626        // read these attributes for all members
627        $attrib = $count ? array('dn') : array_values($this->fieldmap);
628        $attrib[] = 'objectClass';
629        $attrib[] = 'member';
630        $attrib[] = 'uniqueMember';
631        $attrib[] = 'memberURL';
632
633        for ($i=0; $i < $entry[$attr]['count']; $i++)
634        {
635            if (empty($entry[$attr][$i]))
636                continue;
637
638            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
639                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
640
641            $members = @ldap_get_entries($this->conn, $result);
642            if ($members == false)
643            {
644                $this->_debug("S: ".ldap_error($this->conn));
645                $members = array();
646            }
647
648            // for nested groups, call recursively
649            $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
650
651            unset($members['count']);
652            $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
653        }
654
655        return $group_members;
656    }
657
658    /**
659     * List members of group class groupOfUrls
660     *
661     * @param string Group DN
662     * @param array  Group entry
663     * @param boolean True if only used for counting
664     * @return array Accumulated group members
665     */
666    private function _list_group_memberurl($dn, $entry, $count)
667    {
668        $group_members = array();
669
670        for ($i=0; $i < $entry['memberurl']['count']; $i++)
671        {
672            // extract components from url
673            if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
674                continue;
675
676            // add search filter if any
677            $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
678            $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
679
680            $attrib = $count ? array('dn') : array_values($this->fieldmap);
681            if ($result = @$func($this->conn, $m[1], $filter,
682                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
683            ) {
684                $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
685            }
686            else {
687                $this->_debug("S: ".ldap_error($this->conn));
688                return $group_members;
689            }
690
691            $entries = @ldap_get_entries($this->conn, $result);
692            for ($j = 0; $j < $entries['count']; $j++)
693            {
694                if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
695                    $group_members = array_merge($group_members, $nested_group_members);
696                else
697                    $group_members[] = $entries[$j];
698            }
699        }
700
701        return $group_members;
702    }
703
704    /**
705     * Callback for sorting entries
706     */
707    function _entry_sort_cmp($a, $b)
708    {
709        return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
710    }
711
712
713    /**
714     * Search contacts
715     *
716     * @param mixed   $fields   The field name of array of field names to search in
717     * @param mixed   $value    Search value (or array of values when $fields is array)
718     * @param int     $mode     Matching mode:
719     *                          0 - partial (*abc*),
720     *                          1 - strict (=),
721     *                          2 - prefix (abc*)
722     * @param boolean $select   True if results are requested, False if count only
723     * @param boolean $nocount  (Not used)
724     * @param array   $required List of fields that cannot be empty
725     *
726     * @return array  Indexed list of contact records and 'count' value
727     */
728    function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
729    {
730        $mode = intval($mode);
731
732        // special treatment for ID-based search
733        if ($fields == 'ID' || $fields == $this->primary_key)
734        {
735            $ids = !is_array($value) ? explode(',', $value) : $value;
736            $result = new rcube_result_set();
737            foreach ($ids as $id)
738            {
739                if ($rec = $this->get_record($id, true))
740                {
741                    $result->add($rec);
742                    $result->count++;
743                }
744            }
745            return $result;
746        }
747
748        // use VLV pseudo-search for autocompletion
749        if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == 'email,name')
750        {
751            // add general filter to query
752            if (!empty($this->prop['filter']) && empty($this->filter))
753                $this->set_search_set($this->prop['filter']);
754
755            // set VLV controls with encoded search string
756            $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
757
758            $function = $this->_scope2func($this->prop['scope']);
759            $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
760                array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
761
762            $this->result = new rcube_result_set(0);
763
764            if (!$this->ldap_result) {
765                $this->_debug("S: ".ldap_error($this->conn));
766                return $this->result;
767            }
768
769            $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
770
771            // get all entries of this page and post-filter those that really match the query
772            $search = mb_strtolower($value);
773            $entries = ldap_get_entries($this->conn, $this->ldap_result);
774
775            for ($i = 0; $i < $entries['count']; $i++) {
776                $rec = $this->_ldap2result($entries[$i]);
777                foreach (array('email', 'name') as $f) {
778                    $val = mb_strtolower($rec[$f]);
779                    switch ($mode) {
780                    case 1:
781                        $got = ($val == $search);
782                        break;
783                    case 2:
784                        $got = ($search == substr($val, 0, strlen($search)));
785                        break;
786                    default:
787                        $got = (strpos($val, $search) !== false);
788                        break;
789                    }
790
791                    if ($got) {
792                        $this->result->add($rec);
793                        $this->result->count++;
794                        break;
795                    }
796                }
797            }
798
799            return $this->result;
800        }
801
802        // use AND operator for advanced searches
803        $filter = is_array($value) ? '(&' : '(|';
804        // set wildcards
805        $wp = $ws = '';
806        if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
807            $ws = '*';
808            if (!$mode) {
809                $wp = '*';
810            }
811        }
812
813        if ($fields == '*')
814        {
815            // search_fields are required for fulltext search
816            if (empty($this->prop['search_fields']))
817            {
818                $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
819                $this->result = new rcube_result_set();
820                return $this->result;
821            }
822            if (is_array($this->prop['search_fields']))
823            {
824                foreach ($this->prop['search_fields'] as $field) {
825                    $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
826                }
827            }
828        }
829        else
830        {
831            foreach ((array)$fields as $idx => $field) {
832                $val = is_array($value) ? $value[$idx] : $value;
833                if ($f = $this->_map_field($field)) {
834                    $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
835                }
836            }
837        }
838        $filter .= ')';
839
840        // add required (non empty) fields filter
841        $req_filter = '';
842        foreach ((array)$required as $field)
843            if ($f = $this->_map_field($field))
844                $req_filter .= "($f=*)";
845
846        if (!empty($req_filter))
847            $filter = '(&' . $req_filter . $filter . ')';
848
849        // avoid double-wildcard if $value is empty
850        $filter = preg_replace('/\*+/', '*', $filter);
851
852        // add general filter to query
853        if (!empty($this->prop['filter']))
854            $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
855
856        // set filter string and execute search
857        $this->set_search_set($filter);
858        $this->_exec_search();
859
860        if ($select)
861            $this->list_records();
862        else
863            $this->result = $this->count();
864
865        return $this->result;
866    }
867
868
869    /**
870     * Count number of available contacts in database
871     *
872     * @return object rcube_result_set Resultset with values for 'count' and 'first'
873     */
874    function count()
875    {
876        $count = 0;
877        if ($this->conn && $this->ldap_result) {
878            $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
879        }
880        else if ($this->group_id && $this->group_data['dn']) {
881            $count = count($this->list_group_members($this->group_data['dn'], true));
882        }
883        else if ($this->conn) {
884            // We have a connection but no result set, attempt to get one.
885            if (empty($this->filter)) {
886                // The filter is not set, set it.
887                $this->filter = $this->prop['filter'];
888            }
889
890            $count = (int) $this->_exec_search(true);
891        }
892
893        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
894    }
895
896
897    /**
898     * Return the last result set
899     *
900     * @return object rcube_result_set Current resultset or NULL if nothing selected yet
901     */
902    function get_result()
903    {
904        return $this->result;
905    }
906
907
908    /**
909     * Get a specific contact record
910     *
911     * @param mixed   Record identifier
912     * @param boolean Return as associative array
913     *
914     * @return mixed  Hash array or rcube_result_set with all record fields
915     */
916    function get_record($dn, $assoc=false)
917    {
918        $res = $this->result = null;
919
920        if ($this->conn && $dn)
921        {
922            $dn = self::dn_decode($dn);
923
924            $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
925
926            if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
927                $this->_debug("S: OK");
928
929                $entry = ldap_first_entry($this->conn, $ldap_result);
930
931                if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
932                    $rec = array_change_key_case($rec, CASE_LOWER);
933                }
934            }
935            else {
936                $this->_debug("S: ".ldap_error($this->conn));
937            }
938
939            // Use ldap_list to get subentries like country (c) attribute (#1488123)
940            if (!empty($rec) && $this->sub_filter) {
941                if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
942                    foreach ($entries as $entry) {
943                        $lrec = array_change_key_case($entry, CASE_LOWER);
944                        $rec  = array_merge($lrec, $rec);
945                    }
946                }
947            }
948
949            if (!empty($rec)) {
950                // Add in the dn for the entry.
951                $rec['dn'] = $dn;
952                $res = $this->_ldap2result($rec);
953                $this->result = new rcube_result_set();
954                $this->result->add($res);
955            }
956        }
957
958        return $assoc ? $res : $this->result;
959    }
960
961
962    /**
963     * Check the given data before saving.
964     * If input not valid, the message to display can be fetched using get_error()
965     *
966     * @param array Assoziative array with data to save
967     * @param boolean Try to fix/complete record automatically
968     * @return boolean True if input is valid, False if not.
969     */
970    public function validate(&$save_data, $autofix = false)
971    {
972        // validate e-mail addresses
973        if (!parent::validate($save_data, $autofix)) {
974            return false;
975        }
976
977        // check for name input
978        if (empty($save_data['name'])) {
979            $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
980            return false;
981        }
982
983        // Verify that the required fields are set.
984        $missing = null;
985        $ldap_data = $this->_map_data($save_data);
986        foreach ($this->prop['required_fields'] as $fld) {
987            if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
988                $missing[$fld] = 1;
989            }
990        }
991
992        if ($missing) {
993            // try to complete record automatically
994            if ($autofix) {
995                $sn_field    = $this->fieldmap['surname'];
996                $fn_field    = $this->fieldmap['firstname'];
997                $mail_field  = $this->fieldmap['email'];
998
999                // try to extract surname and firstname from displayname
1000                $reverse_map = array_flip($this->fieldmap);
1001                $name_parts  = preg_split('/[\s,.]+/', $save_data['name']);
1002
1003                if ($sn_field && $missing[$sn_field]) {
1004                    $save_data['surname'] = array_pop($name_parts);
1005                    unset($missing[$sn_field]);
1006                }
1007
1008                if ($fn_field && $missing[$fn_field]) {
1009                    $save_data['firstname'] = array_shift($name_parts);
1010                    unset($missing[$fn_field]);
1011                }
1012
1013                // try to fix missing e-mail, very often on import
1014                // from vCard we have email:other only defined
1015                if ($mail_field && $missing[$mail_field]) {
1016                    $emails = $this->get_col_values('email', $save_data, true);
1017                    if (!empty($emails) && ($email = array_shift($emails))) {
1018                        $save_data['email'] = $email;
1019                        unset($missing[$mail_field]);
1020                    }
1021                }
1022            }
1023
1024            // TODO: generate message saying which fields are missing
1025            if (!empty($missing)) {
1026                $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1027                return false;
1028            }
1029        }
1030
1031        return true;
1032    }
1033
1034
1035    /**
1036     * Create a new contact record
1037     *
1038     * @param array    Hash array with save data
1039     *
1040     * @return encoded record ID on success, False on error
1041     */
1042    function insert($save_cols)
1043    {
1044        // Map out the column names to their LDAP ones to build the new entry.
1045        $newentry = $this->_map_data($save_cols);
1046        $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
1047
1048        // Verify that the required fields are set.
1049        $missing = null;
1050        foreach ($this->prop['required_fields'] as $fld) {
1051            if (!isset($newentry[$fld])) {
1052                $missing[] = $fld;
1053            }
1054        }
1055
1056        // abort process if requiered fields are missing
1057        // TODO: generate message saying which fields are missing
1058        if ($missing) {
1059            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1060            return false;
1061        }
1062
1063        // Build the new entries DN.
1064        $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
1065
1066        // Remove attributes that need to be added separately (child objects)
1067        $xfields = array();
1068        if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
1069            foreach ($this->prop['sub_fields'] as $xf => $xclass) {
1070                if (!empty($newentry[$xf])) {
1071                    $xfields[$xf] = $newentry[$xf];
1072                    unset($newentry[$xf]);
1073                }
1074            }
1075        }
1076
1077        if (!$this->ldap_add($dn, $newentry)) {
1078            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1079            return false;
1080        }
1081
1082        foreach ($xfields as $xidx => $xf) {
1083            $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
1084            $xf = array(
1085                $xidx => $xf,
1086                'objectClass' => (array) $this->prop['sub_fields'][$xidx],
1087            );
1088
1089            $this->ldap_add($xdn, $xf);
1090        }
1091
1092        $dn = self::dn_encode($dn);
1093
1094        // add new contact to the selected group
1095        if ($this->group_id)
1096            $this->add_to_group($this->group_id, $dn);
1097
1098        return $dn;
1099    }
1100
1101
1102    /**
1103     * Update a specific contact record
1104     *
1105     * @param mixed Record identifier
1106     * @param array Hash array with save data
1107     *
1108     * @return boolean True on success, False on error
1109     */
1110    function update($id, $save_cols)
1111    {
1112        $record = $this->get_record($id, true);
1113
1114        $newdata     = array();
1115        $replacedata = array();
1116        $deletedata  = array();
1117        $subdata     = array();
1118        $subdeldata  = array();
1119        $subnewdata  = array();
1120
1121        $ldap_data = $this->_map_data($save_cols);
1122        $old_data  = $record['_raw_attrib'];
1123
1124        // special handling of photo col
1125        if ($photo_fld = $this->fieldmap['photo']) {
1126            // undefined means keep old photo
1127            if (!array_key_exists('photo', $save_cols)) {
1128                $ldap_data[$photo_fld] = $record['photo'];
1129            }
1130        }
1131
1132        foreach ($this->fieldmap as $col => $fld) {
1133            if ($fld) {
1134                $val = $ldap_data[$fld];
1135                $old = $old_data[$fld];
1136                // remove empty array values
1137                if (is_array($val))
1138                    $val = array_filter($val);
1139                // $this->_map_data() result and _raw_attrib use different format
1140                // make sure comparing array with one element with a string works as expected
1141                if (is_array($old) && count($old) == 1 && !is_array($val)) {
1142                    $old = array_pop($old);
1143                }
1144                if (is_array($val) && count($val) == 1 && !is_array($old)) {
1145                    $val = array_pop($val);
1146                }
1147                // Subentries must be handled separately
1148                if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
1149                    if ($old != $val) {
1150                        if ($old !== null) {
1151                            $subdeldata[$fld] = $old;
1152                        }
1153                        if ($val) {
1154                            $subnewdata[$fld] = $val;
1155                        }
1156                    }
1157                    else if ($old !== null) {
1158                        $subdata[$fld] = $old;
1159                    }
1160                    continue;
1161                }
1162
1163                // The field does exist compare it to the ldap record.
1164                if ($old != $val) {
1165                    // Changed, but find out how.
1166                    if ($old === null) {
1167                        // Field was not set prior, need to add it.
1168                        $newdata[$fld] = $val;
1169                    }
1170                    else if ($val == '') {
1171                        // Field supplied is empty, verify that it is not required.
1172                        if (!in_array($fld, $this->prop['required_fields'])) {
1173                            // It is not, safe to clear.
1174                            $deletedata[$fld] = $old_data[$fld];
1175                        }
1176                    }
1177                    else {
1178                        // The data was modified, save it out.
1179                        $replacedata[$fld] = $val;
1180                    }
1181                } // end if
1182            } // end if
1183        } // end foreach
1184/*
1185        console($old_data);
1186        console($ldap_data);
1187        console('----');
1188        console($newdata);
1189        console($replacedata);
1190        console($deletedata);
1191        console('----');
1192        console($subdata);
1193        console($subnewdata);
1194        console($subdeldata);
1195*/
1196        $dn = self::dn_decode($id);
1197
1198        // Update the entry as required.
1199        if (!empty($deletedata)) {
1200            // Delete the fields.
1201            if (!$this->ldap_mod_del($dn, $deletedata)) {
1202                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1203                return false;
1204            }
1205        } // end if
1206
1207        if (!empty($replacedata)) {
1208            // Handle RDN change
1209            if ($replacedata[$this->prop['LDAP_rdn']]) {
1210                $newdn = $this->prop['LDAP_rdn'].'='
1211                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
1212                    .','.$this->base_dn;
1213                if ($dn != $newdn) {
1214                    $newrdn = $this->prop['LDAP_rdn'].'='
1215                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
1216                    unset($replacedata[$this->prop['LDAP_rdn']]);
1217                }
1218            }
1219            // Replace the fields.
1220            if (!empty($replacedata)) {
1221                if (!$this->ldap_mod_replace($dn, $replacedata)) {
1222                    $this->set_error(self::ERROR_SAVING, 'errorsaving');
1223                    return false;
1224                }
1225            }
1226        } // end if
1227
1228        // RDN change, we need to remove all sub-entries
1229        if (!empty($newrdn)) {
1230            $subdeldata = array_merge($subdeldata, $subdata);
1231            $subnewdata = array_merge($subnewdata, $subdata);
1232        }
1233
1234        // remove sub-entries
1235        if (!empty($subdeldata)) {
1236            foreach ($subdeldata as $fld => $val) {
1237                $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
1238                if (!$this->ldap_delete($subdn)) {
1239                    return false;
1240                }
1241            }
1242        }
1243
1244        if (!empty($newdata)) {
1245            // Add the fields.
1246            if (!$this->ldap_mod_add($dn, $newdata)) {
1247                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1248                return false;
1249            }
1250        } // end if
1251
1252        // Handle RDN change
1253        if (!empty($newrdn)) {
1254            if (!$this->ldap_rename($dn, $newrdn, null, true)) {
1255                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1256                return false;
1257            }
1258
1259            $dn    = self::dn_encode($dn);
1260            $newdn = self::dn_encode($newdn);
1261
1262            // change the group membership of the contact
1263            if ($this->groups) {
1264                $group_ids = $this->get_record_groups($dn);
1265                foreach ($group_ids as $group_id)
1266                {
1267                    $this->remove_from_group($group_id, $dn);
1268                    $this->add_to_group($group_id, $newdn);
1269                }
1270            }
1271
1272            $dn = self::dn_decode($newdn);
1273        }
1274
1275        // add sub-entries
1276        if (!empty($subnewdata)) {
1277            foreach ($subnewdata as $fld => $val) {
1278                $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
1279                $xf = array(
1280                    $fld => $val,
1281                    'objectClass' => (array) $this->prop['sub_fields'][$fld],
1282                );
1283                $this->ldap_add($subdn, $xf);
1284            }
1285        }
1286
1287        return $newdn ? $newdn : true;
1288    }
1289
1290
1291    /**
1292     * Mark one or more contact records as deleted
1293     *
1294     * @param array   Record identifiers
1295     * @param boolean Remove record(s) irreversible (unsupported)
1296     *
1297     * @return boolean True on success, False on error
1298     */
1299    function delete($ids, $force=true)
1300    {
1301        if (!is_array($ids)) {
1302            // Not an array, break apart the encoded DNs.
1303            $ids = explode(',', $ids);
1304        } // end if
1305
1306        foreach ($ids as $id) {
1307            $dn = self::dn_decode($id);
1308
1309            // Need to delete all sub-entries first
1310            if ($this->sub_filter) {
1311                if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
1312                    foreach ($entries as $entry) {
1313                        if (!$this->ldap_delete($entry['dn'])) {
1314                            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1315                            return false;
1316                        }
1317                    }
1318                }
1319            }
1320
1321            // Delete the record.
1322            if (!$this->ldap_delete($dn)) {
1323                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1324                return false;
1325            }
1326
1327            // remove contact from all groups where he was member
1328            if ($this->groups) {
1329                $dn = self::dn_encode($dn);
1330                $group_ids = $this->get_record_groups($dn);
1331                foreach ($group_ids as $group_id) {
1332                    $this->remove_from_group($group_id, $dn);
1333                }
1334            }
1335        } // end foreach
1336
1337        return count($ids);
1338    }
1339
1340
1341    /**
1342     * Remove all contact records
1343     */
1344    function delete_all()
1345    {
1346        //searching for contact entries
1347        $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
1348
1349        if (!empty($dn_list)) {
1350            foreach ($dn_list as $idx => $entry) {
1351                $dn_list[$idx] = self::dn_encode($entry['dn']);
1352            }
1353            $this->delete($dn_list);
1354        }
1355    }
1356
1357
1358    /**
1359     * Execute the LDAP search based on the stored credentials
1360     */
1361    private function _exec_search($count = false)
1362    {
1363        if ($this->ready)
1364        {
1365            $filter = $this->filter ? $this->filter : '(objectclass=*)';
1366            $function = $this->_scope2func($this->prop['scope'], $ns_function);
1367
1368            $this->_debug("C: Search [$filter][dn: $this->base_dn]");
1369
1370            // when using VLV, we get the total count by...
1371            if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
1372                // ...either reading numSubOrdinates attribute
1373                if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
1374                    $counts = ldap_get_entries($this->conn, $result_count);
1375                    for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
1376                        $this->vlv_count += $counts[$j]['numsubordinates'][0];
1377                    $this->_debug("D: total numsubordinates = " . $this->vlv_count);
1378                }
1379                else if (!function_exists('ldap_parse_virtuallist_control'))  // ...or by fetching all records dn and count them
1380                    $this->vlv_count = $this->_exec_search(true);
1381
1382                $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
1383            }
1384
1385            // only fetch dn for count (should keep the payload low)
1386            $attrs = $count ? array('dn') : array_values($this->fieldmap);
1387            if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
1388                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
1389            ) {
1390                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
1391                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
1392                    if (ldap_parse_result($this->conn, $this->ldap_result,
1393                        $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
1394                    ) {
1395                        ldap_parse_virtuallist_control($this->conn, $serverctrls,
1396                            $last_offset, $this->vlv_count, $vresult);
1397                        $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
1398                    }
1399                    else {
1400                        $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
1401                    }
1402                }
1403
1404                $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
1405                $this->_debug("S: $entries_count record(s)");
1406
1407                return $count ? $entries_count : true;
1408            }
1409            else {
1410                $this->_debug("S: ".ldap_error($this->conn));
1411            }
1412        }
1413
1414        return false;
1415    }
1416
1417    /**
1418     * Choose the right PHP function according to scope property
1419     */
1420    private function _scope2func($scope, &$ns_function = null)
1421    {
1422        switch ($scope) {
1423          case 'sub':
1424            $function = $ns_function  = 'ldap_search';
1425            break;
1426          case 'base':
1427            $function = $ns_function = 'ldap_read';
1428            break;
1429          default:
1430            $function = 'ldap_list';
1431            $ns_function = 'ldap_read';
1432            break;
1433        }
1434
1435        return $function;
1436    }
1437
1438    /**
1439     * Set server controls for Virtual List View (paginated listing)
1440     */
1441    private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
1442    {
1443        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$prop['sort']));
1444        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
1445
1446        $sort = (array)$prop['sort'];
1447        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
1448            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
1449
1450        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
1451            $this->_debug("S: ".ldap_error($this->conn));
1452            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
1453            return false;
1454        }
1455
1456        return true;
1457    }
1458
1459
1460    /**
1461     * Converts LDAP entry into an array
1462     */
1463    private function _ldap2result($rec)
1464    {
1465        $out = array();
1466
1467        if ($rec['dn'])
1468            $out[$this->primary_key] = self::dn_encode($rec['dn']);
1469
1470        foreach ($this->fieldmap as $rf => $lf)
1471        {
1472            for ($i=0; $i < $rec[$lf]['count']; $i++) {
1473                if (!($value = $rec[$lf][$i]))
1474                    continue;
1475
1476                list($col, $subtype) = explode(':', $rf);
1477                $out['_raw_attrib'][$lf][$i] = $value;
1478
1479                if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
1480                    $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
1481                else if (in_array($col, array('street','zipcode','locality','country','region')))
1482                    $out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
1483                else if ($rec[$lf]['count'] > 1)
1484                    $out[$rf][] = $value;
1485                else
1486                    $out[$rf] = $value;
1487            }
1488
1489            // Make sure name fields aren't arrays (#1488108)
1490            if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
1491                $out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
1492            }
1493        }
1494
1495        return $out;
1496    }
1497
1498
1499    /**
1500     * Return real field name (from fields map)
1501     */
1502    private function _map_field($field)
1503    {
1504        return $this->fieldmap[$field];
1505    }
1506
1507
1508    /**
1509     * Convert a record data set into LDAP field attributes
1510     */
1511    private function _map_data($save_cols)
1512    {
1513        // flatten composite fields first
1514        foreach ($this->coltypes as $col => $colprop) {
1515            if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
1516                foreach ($values as $subtype => $childs) {
1517                    $subtype = $subtype ? ':'.$subtype : '';
1518                    foreach ($childs as $i => $child_values) {
1519                        foreach ((array)$child_values as $childcol => $value) {
1520                            $save_cols[$childcol.$subtype][$i] = $value;
1521                        }
1522                    }
1523                }
1524            }
1525        }
1526
1527        $ldap_data = array();
1528        foreach ($this->fieldmap as $col => $fld) {
1529            $val = $save_cols[$col];
1530            if (is_array($val))
1531                $val = array_filter($val);  // remove empty entries
1532            if ($fld && $val) {
1533                // The field does exist, add it to the entry.
1534                $ldap_data[$fld] = $val;
1535            }
1536        }
1537
1538        return $ldap_data;
1539    }
1540
1541
1542    /**
1543     * Returns unified attribute name (resolving aliases)
1544     */
1545    private static function _attr_name($name)
1546    {
1547        // list of known attribute aliases
1548        $aliases = array(
1549            'gn' => 'givenname',
1550            'rfc822mailbox' => 'email',
1551            'userid' => 'uid',
1552            'emailaddress' => 'email',
1553            'pkcs9email' => 'email',
1554        );
1555        return isset($aliases[$name]) ? $aliases[$name] : $name;
1556    }
1557
1558
1559    /**
1560     * Prints debug info to the log
1561     */
1562    private function _debug($str)
1563    {
1564        if ($this->debug) {
1565            rcmail::write_log('ldap', $str);
1566        }
1567    }
1568
1569
1570    /**
1571     * Activate/deactivate debug mode
1572     *
1573     * @param boolean $dbg True if LDAP commands should be logged
1574     * @access public
1575     */
1576    function set_debug($dbg = true)
1577    {
1578        $this->debug = $dbg;
1579    }
1580
1581
1582    /**
1583     * Quotes attribute value string
1584     *
1585     * @param string $str Attribute value
1586     * @param bool   $dn  True if the attribute is a DN
1587     *
1588     * @return string Quoted string
1589     */
1590    private static function _quote_string($str, $dn=false)
1591    {
1592        // take firt entry if array given
1593        if (is_array($str))
1594            $str = reset($str);
1595
1596        if ($dn)
1597            $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
1598                '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
1599        else
1600            $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
1601                '/'=>'\2f');
1602
1603        return strtr($str, $replace);
1604    }
1605
1606
1607    /**
1608     * Setter for the current group
1609     * (empty, has to be re-implemented by extending class)
1610     */
1611    function set_group($group_id)
1612    {
1613        if ($group_id)
1614        {
1615            if (($group_cache = $this->cache->get('groups')) === null)
1616                $group_cache = $this->_fetch_groups();
1617
1618            $this->group_id = $group_id;
1619            $this->group_data = $group_cache[$group_id];
1620        }
1621        else
1622        {
1623            $this->group_id = 0;
1624            $this->group_data = null;
1625        }
1626    }
1627
1628    /**
1629     * List all active contact groups of this source
1630     *
1631     * @param string  Optional search string to match group name
1632     * @return array  Indexed list of contact groups, each a hash array
1633     */
1634    function list_groups($search = null)
1635    {
1636        if (!$this->groups)
1637            return array();
1638
1639        // use cached list for searching
1640        $this->cache->expunge();
1641        if (!$search || ($group_cache = $this->cache->get('groups')) === null)
1642            $group_cache = $this->_fetch_groups();
1643
1644        $groups = array();
1645        if ($search) {
1646            $search = mb_strtolower($search);
1647            foreach ($group_cache as $group) {
1648                if (strpos(mb_strtolower($group['name']), $search) !== false)
1649                    $groups[] = $group;
1650            }
1651        }
1652        else
1653            $groups = $group_cache;
1654
1655        return array_values($groups);
1656    }
1657
1658    /**
1659     * Fetch groups from server
1660     */
1661    private function _fetch_groups($vlv_page = 0)
1662    {
1663        $base_dn = $this->groups_base_dn;
1664        $filter = $this->prop['groups']['filter'];
1665        $name_attr = $this->prop['groups']['name_attr'];
1666        $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
1667        $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
1668        $sort_attr = $sort_attrs[0];
1669
1670        $this->_debug("C: Search [$filter][dn: $base_dn]");
1671
1672        // use vlv to list groups
1673        if ($this->prop['groups']['vlv']) {
1674            $page_size = 200;
1675            if (!$this->prop['groups']['sort'])
1676                $this->prop['groups']['sort'] = $sort_attrs;
1677            $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
1678        }
1679
1680        $function = $this->_scope2func($this->prop['groups']['scope'], $ns_function);
1681        $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
1682        if ($res === false)
1683        {
1684            $this->_debug("S: ".ldap_error($this->conn));
1685            return array();
1686        }
1687
1688        $ldap_data = ldap_get_entries($this->conn, $res);
1689        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
1690
1691        $groups = array();
1692        $group_sortnames = array();
1693        $group_count = $ldap_data["count"];
1694        for ($i=0; $i < $group_count; $i++)
1695        {
1696            $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
1697            $group_id = self::dn_encode($group_name);
1698            $groups[$group_id]['ID'] = $group_id;
1699            $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
1700            $groups[$group_id]['name'] = $group_name;
1701            $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
1702
1703            // list email attributes of a group
1704            for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
1705                if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
1706                    $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
1707            }
1708
1709            $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
1710        }
1711
1712        // recursive call can exit here
1713        if ($vlv_page > 0)
1714            return $groups;
1715
1716        // call recursively until we have fetched all groups
1717        while ($vlv_active && $group_count == $page_size)
1718        {
1719            $next_page = $this->_fetch_groups(++$vlv_page);
1720            $groups = array_merge($groups, $next_page);
1721            $group_count = count($next_page);
1722        }
1723
1724        // when using VLV the list of groups is already sorted
1725        if (!$this->prop['groups']['vlv'])
1726            array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
1727
1728        // cache this
1729        $this->cache->set('groups', $groups);
1730
1731        return $groups;
1732    }
1733
1734    /**
1735     * Get group properties such as name and email address(es)
1736     *
1737     * @param string Group identifier
1738     * @return array Group properties as hash array
1739     */
1740    function get_group($group_id)
1741    {
1742        if (($group_cache = $this->cache->get('groups')) === null)
1743            $group_cache = $this->_fetch_groups();
1744
1745        $group_data = $group_cache[$group_id];
1746        unset($group_data['dn'], $group_data['member_attr']);
1747
1748        return $group_data;
1749    }
1750
1751    /**
1752     * Create a contact group with the given name
1753     *
1754     * @param string The group name
1755     * @return mixed False on error, array with record props in success
1756     */
1757    function create_group($group_name)
1758    {
1759        $base_dn = $this->groups_base_dn;
1760        $new_dn = "cn=$group_name,$base_dn";
1761        $new_gid = self::dn_encode($group_name);
1762        $member_attr = $this->get_group_member_attr();
1763        $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
1764
1765        $new_entry = array(
1766            'objectClass' => $this->prop['groups']['object_classes'],
1767            $name_attr => $group_name,
1768            $member_attr => '',
1769        );
1770
1771        if (!$this->ldap_add($new_dn, $new_entry)) {
1772            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1773            return false;
1774        }
1775
1776        $this->cache->remove('groups');
1777
1778        return array('id' => $new_gid, 'name' => $group_name);
1779    }
1780
1781    /**
1782     * Delete the given group and all linked group members
1783     *
1784     * @param string Group identifier
1785     * @return boolean True on success, false if no data was changed
1786     */
1787    function delete_group($group_id)
1788    {
1789        if (($group_cache = $this->cache->get('groups')) === null)
1790            $group_cache = $this->_fetch_groups();
1791
1792        $base_dn = $this->groups_base_dn;
1793        $group_name = $group_cache[$group_id]['name'];
1794        $del_dn = "cn=$group_name,$base_dn";
1795
1796        if (!$this->ldap_delete($del_dn)) {
1797            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1798            return false;
1799        }
1800
1801        $this->cache->remove('groups');
1802
1803        return true;
1804    }
1805
1806    /**
1807     * Rename a specific contact group
1808     *
1809     * @param string Group identifier
1810     * @param string New name to set for this group
1811     * @param string New group identifier (if changed, otherwise don't set)
1812     * @return boolean New name on success, false if no data was changed
1813     */
1814    function rename_group($group_id, $new_name, &$new_gid)
1815    {
1816        if (($group_cache = $this->cache->get('groups')) === null)
1817            $group_cache = $this->_fetch_groups();
1818
1819        $base_dn = $this->groups_base_dn;
1820        $group_name = $group_cache[$group_id]['name'];
1821        $old_dn = "cn=$group_name,$base_dn";
1822        $new_rdn = "cn=$new_name";
1823        $new_gid = self::dn_encode($new_name);
1824
1825        if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
1826            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1827            return false;
1828        }
1829
1830        $this->cache->remove('groups');
1831
1832        return $new_name;
1833    }
1834
1835    /**
1836     * Add the given contact records the a certain group
1837     *
1838     * @param string  Group identifier
1839     * @param array   List of contact identifiers to be added
1840     * @return int    Number of contacts added
1841     */
1842    function add_to_group($group_id, $contact_ids)
1843    {
1844        if (($group_cache = $this->cache->get('groups')) === null)
1845            $group_cache = $this->_fetch_groups();
1846
1847        if (!is_array($contact_ids))
1848            $contact_ids = explode(',', $contact_ids);
1849
1850        $base_dn     = $this->groups_base_dn;
1851        $group_name  = $group_cache[$group_id]['name'];
1852        $member_attr = $group_cache[$group_id]['member_attr'];
1853        $group_dn    = "cn=$group_name,$base_dn";
1854
1855        $new_attrs = array();
1856        foreach ($contact_ids as $id)
1857            $new_attrs[$member_attr][] = self::dn_decode($id);
1858
1859        if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
1860            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1861            return 0;
1862        }
1863
1864        $this->cache->remove('groups');
1865
1866        return count($new_attrs['member']);
1867    }
1868
1869    /**
1870     * Remove the given contact records from a certain group
1871     *
1872     * @param string  Group identifier
1873     * @param array   List of contact identifiers to be removed
1874     * @return int    Number of deleted group members
1875     */
1876    function remove_from_group($group_id, $contact_ids)
1877    {
1878        if (($group_cache = $this->cache->get('groups')) === null)
1879            $group_cache = $this->_fetch_groups();
1880
1881        $base_dn     = $this->groups_base_dn;
1882        $group_name  = $group_cache[$group_id]['name'];
1883        $member_attr = $group_cache[$group_id]['member_attr'];
1884        $group_dn    = "cn=$group_name,$base_dn";
1885
1886        $del_attrs = array();
1887        foreach (explode(",", $contact_ids) as $id)
1888            $del_attrs[$member_attr][] = self::dn_decode($id);
1889
1890        if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
1891            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1892            return 0;
1893        }
1894
1895        $this->cache->remove('groups');
1896
1897        return count($del_attrs['member']);
1898    }
1899
1900    /**
1901     * Get group assignments of a specific contact record
1902     *
1903     * @param mixed Record identifier
1904     *
1905     * @return array List of assigned groups as ID=>Name pairs
1906     * @since 0.5-beta
1907     */
1908    function get_record_groups($contact_id)
1909    {
1910        if (!$this->groups)
1911            return array();
1912
1913        $base_dn     = $this->groups_base_dn;
1914        $contact_dn  = self::dn_decode($contact_id);
1915        $name_attr   = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
1916        $member_attr = $this->get_group_member_attr();
1917        $add_filter  = '';
1918        if ($member_attr != 'member' && $member_attr != 'uniqueMember')
1919            $add_filter = "($member_attr=$contact_dn)";
1920        $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
1921
1922        $this->_debug("C: Search [$filter][dn: $base_dn]");
1923
1924        $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
1925        if ($res === false)
1926        {
1927            $this->_debug("S: ".ldap_error($this->conn));
1928            return array();
1929        }
1930        $ldap_data = ldap_get_entries($this->conn, $res);
1931        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
1932
1933        $groups = array();
1934        for ($i=0; $i<$ldap_data["count"]; $i++)
1935        {
1936            $group_name = $ldap_data[$i][$name_attr][0];
1937            $group_id = self::dn_encode($group_name);
1938            $groups[$group_id] = $group_id;
1939        }
1940        return $groups;
1941    }
1942
1943    /**
1944     * Detects group member attribute name
1945     */
1946    private function get_group_member_attr($object_classes = array())
1947    {
1948        if (empty($object_classes)) {
1949            $object_classes = $this->prop['groups']['object_classes'];
1950        }
1951        if (!empty($object_classes)) {
1952            foreach ((array)$object_classes as $oc) {
1953                switch (strtolower($oc)) {
1954                    case 'group':
1955                    case 'groupofnames':
1956                    case 'kolabgroupofnames':
1957                        $member_attr = 'member';
1958                        break;
1959
1960                    case 'groupofuniquenames':
1961                    case 'kolabgroupofuniquenames':
1962                        $member_attr = 'uniqueMember';
1963                        break;
1964                }
1965            }
1966        }
1967
1968        if (!empty($member_attr)) {
1969            return $member_attr;
1970        }
1971
1972        if (!empty($this->prop['groups']['member_attr'])) {
1973            return $this->prop['groups']['member_attr'];
1974        }
1975
1976        return 'member';
1977    }
1978
1979
1980    /**
1981     * Generate BER encoded string for Virtual List View option
1982     *
1983     * @param integer List offset (first record)
1984     * @param integer Records per page
1985     * @return string BER encoded option value
1986     */
1987    private function _vlv_ber_encode($offset, $rpp, $search = '')
1988    {
1989        # this string is ber-encoded, php will prefix this value with:
1990        # 04 (octet string) and 10 (length of 16 bytes)
1991        # the code behind this string is broken down as follows:
1992        # 30 = ber sequence with a length of 0e (14) bytes following
1993        # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
1994        # 02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
1995        # a0 = type context-specific/constructed with a length of 06 (6) bytes following
1996        # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
1997        # 02 = type integer with 2 bytes following (contentCount):  01 00
1998       
1999        # whith a search string present:
2000        # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
2001        # 81 indicates a user string is present where as a a0 indicates just a offset search
2002        # 81 = type context-specific/constructed with a length of 06 (6) bytes following
2003       
2004        # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
2005        # encoding of integer values (note: these values are in
2006        # two-complement form so since offset will never be negative bit 8 of the
2007        # leftmost octet should never by set to 1):
2008        # 8.3.2: If the contents octets of an integer value encoding consist
2009        # of more than one octet, then the bits of the first octet (rightmost) and bit 8
2010        # of the second (to the left of first octet) octet:
2011        # a) shall not all be ones; and
2012        # b) shall not all be zero
2013       
2014        if ($search)
2015        {
2016            $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
2017            $ber_val = self::_string2hex($search);
2018            $str = self::_ber_addseq($ber_val, '81');
2019        }
2020        else
2021        {
2022            # construct the string from right to left
2023            $str = "020100"; # contentCount
2024
2025            $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
2026
2027            // calculate octet length of $ber_val
2028            $str = self::_ber_addseq($ber_val, '02') . $str;
2029
2030            // now compute length over $str
2031            $str = self::_ber_addseq($str, 'a0');
2032        }
2033       
2034        // now tack on records per page
2035        $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
2036
2037        // now tack on sequence identifier and length
2038        $str = self::_ber_addseq($str, '30');
2039
2040        return pack('H'.strlen($str), $str);
2041    }
2042
2043
2044    /**
2045     * create ber encoding for sort control
2046     *
2047     * @param array List of cols to sort by
2048     * @return string BER encoded option value
2049     */
2050    private function _sort_ber_encode($sortcols)
2051    {
2052        $str = '';
2053        foreach (array_reverse((array)$sortcols) as $col) {
2054            $ber_val = self::_string2hex($col);
2055
2056            # 30 = ber sequence with a length of octet value
2057            # 04 = octet string with a length of the ascii value
2058            $oct = self::_ber_addseq($ber_val, '04');
2059            $str = self::_ber_addseq($oct, '30') . $str;
2060        }
2061
2062        // now tack on sequence identifier and length
2063        $str = self::_ber_addseq($str, '30');
2064
2065        return pack('H'.strlen($str), $str);
2066    }
2067
2068    /**
2069     * Add BER sequence with correct length and the given identifier
2070     */
2071    private static function _ber_addseq($str, $identifier)
2072    {
2073        $len = dechex(strlen($str)/2);
2074        if (strlen($len) % 2 != 0)
2075            $len = '0'.$len;
2076
2077        return $identifier . $len . $str;
2078    }
2079
2080    /**
2081     * Returns BER encoded integer value in hex format
2082     */
2083    private static function _ber_encode_int($offset)
2084    {
2085        $val = dechex($offset);
2086        $prefix = '';
2087
2088        // check if bit 8 of high byte is 1
2089        if (preg_match('/^[89abcdef]/', $val))
2090            $prefix = '00';
2091
2092        if (strlen($val)%2 != 0)
2093            $prefix .= '0';
2094
2095        return $prefix . $val;
2096    }
2097
2098    /**
2099     * Returns ascii string encoded in hex
2100     */
2101    private static function _string2hex($str)
2102    {
2103        $hex = '';
2104        for ($i=0; $i < strlen($str); $i++)
2105            $hex .= dechex(ord($str[$i]));
2106        return $hex;
2107    }
2108
2109    /**
2110     * HTML-safe DN string encoding
2111     *
2112     * @param string $str DN string
2113     *
2114     * @return string Encoded HTML identifier string
2115     */
2116    static function dn_encode($str)
2117    {
2118        // @TODO: to make output string shorter we could probably
2119        //        remove dc=* items from it
2120        return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
2121    }
2122
2123    /**
2124     * Decodes DN string encoded with _dn_encode()
2125     *
2126     * @param string $str Encoded HTML identifier string
2127     *
2128     * @return string DN string
2129     */
2130    static function dn_decode($str)
2131    {
2132        $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
2133        return base64_decode($str);
2134    }
2135
2136    /**
2137     * Wrapper for ldap_add()
2138     */
2139    protected function ldap_add($dn, $entry)
2140    {
2141        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
2142
2143        $res = ldap_add($this->conn, $dn, $entry);
2144        if ($res === false) {
2145            $this->_debug("S: ".ldap_error($this->conn));
2146            return false;
2147        }
2148
2149        $this->_debug("S: OK");
2150        return true;
2151    }
2152
2153    /**
2154     * Wrapper for ldap_delete()
2155     */
2156    protected function ldap_delete($dn)
2157    {
2158        $this->_debug("C: Delete [dn: $dn]");
2159
2160        $res = ldap_delete($this->conn, $dn);
2161        if ($res === false) {
2162            $this->_debug("S: ".ldap_error($this->conn));
2163            return false;
2164        }
2165
2166        $this->_debug("S: OK");
2167        return true;
2168    }
2169
2170    /**
2171     * Wrapper for ldap_mod_replace()
2172     */
2173    protected function ldap_mod_replace($dn, $entry)
2174    {
2175        $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
2176
2177        if (!ldap_mod_replace($this->conn, $dn, $entry)) {
2178            $this->_debug("S: ".ldap_error($this->conn));
2179            return false;
2180        }
2181
2182        $this->_debug("S: OK");
2183        return true;
2184    }
2185
2186    /**
2187     * Wrapper for ldap_mod_add()
2188     */
2189    protected function ldap_mod_add($dn, $entry)
2190    {
2191        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
2192
2193        if (!ldap_mod_add($this->conn, $dn, $entry)) {
2194            $this->_debug("S: ".ldap_error($this->conn));
2195            return false;
2196        }
2197
2198        $this->_debug("S: OK");
2199        return true;
2200    }
2201
2202    /**
2203     * Wrapper for ldap_mod_del()
2204     */
2205    protected function ldap_mod_del($dn, $entry)
2206    {
2207        $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
2208
2209        if (!ldap_mod_del($this->conn, $dn, $entry)) {
2210            $this->_debug("S: ".ldap_error($this->conn));
2211            return false;
2212        }
2213
2214        $this->_debug("S: OK");
2215        return true;
2216    }
2217
2218    /**
2219     * Wrapper for ldap_rename()
2220     */
2221    protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
2222    {
2223        $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
2224
2225        if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
2226            $this->_debug("S: ".ldap_error($this->conn));
2227            return false;
2228        }
2229
2230        $this->_debug("S: OK");
2231        return true;
2232    }
2233
2234    /**
2235     * Wrapper for ldap_list()
2236     */
2237    protected function ldap_list($dn, $filter, $attrs = array(''))
2238    {
2239        $list = array();
2240        $this->_debug("C: List [dn: $dn] [{$filter}]");
2241
2242        if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
2243            $list = ldap_get_entries($this->conn, $result);
2244
2245            if ($list === false) {
2246                $this->_debug("S: ".ldap_error($this->conn));
2247                return array();
2248            }
2249
2250            $count = $list['count'];
2251            unset($list['count']);
2252
2253            $this->_debug("S: $count record(s)");
2254        }
2255        else {
2256            $this->_debug("S: ".ldap_error($this->conn));
2257        }
2258
2259        return $list;
2260    }
2261
2262}
Note: See TracBrowser for help on using the repository browser.