alecpl
2012-04-14 9870dac30de1b8d1bdeab57af3eeb7c89b21ef76
commit | author | age
d1d2c4 1 <?php
S 2 /*
3  +-----------------------------------------------------------------------+
47124c 4  | program/include/rcube_ldap.php                                        |
d1d2c4 5  |                                                                       |
e019f2 6  | This file is part of the Roundcube Webmail client                     |
438753 7  | Copyright (C) 2006-2012, The Roundcube Dev Team                       |
c3ba0e 8  | Copyright (C) 2011, Kolab Systems AG                                  |
7fe381 9  |                                                                       |
T 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.                     |
d1d2c4 13  |                                                                       |
S 14  | PURPOSE:                                                              |
f11541 15  |   Interface to an LDAP address directory                              |
d1d2c4 16  |                                                                       |
S 17  +-----------------------------------------------------------------------+
f11541 18  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
6039aa 19  |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
c3ba0e 20  |         Aleksander Machniak <machniak@kolabsys.com>                   |
d1d2c4 21  +-----------------------------------------------------------------------+
S 22
23  $Id$
24
25 */
26
6d969b 27
T 28 /**
29  * Model class to access an LDAP address directory
30  *
31  * @package Addressbook
32  */
cc97ea 33 class rcube_ldap extends rcube_addressbook
f11541 34 {
a97937 35     /** public properties */
T 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();
1148c6 42
a97937 43     /** private properties */
T 44     protected $conn;
45     protected $prop = array();
46     protected $fieldmap = array();
13db9e 47     protected $sub_filter;
a97937 48     protected $filter = '';
T 49     protected $result = null;
50     protected $ldap_result = null;
51     protected $mail_domain = '';
52     protected $debug = false;
6039aa 53
d1e08f 54     private $base_dn = '';
T 55     private $groups_base_dn = '';
8fb04b 56     private $group_url = null;
T 57     private $cache;
1148c6 58
69ea3a 59     private $vlv_active = false;
T 60     private $vlv_count = 0;
61
1148c6 62
a97937 63     /**
T 64     * Object constructor
65     *
0c2596 66     * @param array      $p            LDAP connection properties
A 67     * @param boolean $debug        Enables debug mode
68     * @param string  $mail_domain  Current user mail domain name
a97937 69     */
0c2596 70     function __construct($p, $debug = false, $mail_domain = null)
f11541 71     {
a97937 72         $this->prop = $p;
b1f084 73
2d3e2b 74         if (isset($p['searchonly']))
T 75             $this->searchonly = $p['searchonly'];
010274 76
a97937 77         // check if groups are configured
f763fb 78         if (is_array($p['groups']) && count($p['groups'])) {
a97937 79             $this->groups = true;
f763fb 80             // set member field
A 81             if (!empty($p['groups']['member_attr']))
015dec 82                 $this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
f763fb 83             else if (empty($p['member_attr']))
A 84                 $this->prop['member_attr'] = 'member';
448f81 85             // set default name attribute to cn
T 86             if (empty($this->prop['groups']['name_attr']))
87                 $this->prop['groups']['name_attr'] = 'cn';
fb6cc8 88             if (empty($this->prop['groups']['scope']))
T 89                 $this->prop['groups']['scope'] = 'sub';
f763fb 90         }
cd6749 91
a97937 92         // fieldmap property is given
T 93         if (is_array($p['fieldmap'])) {
94             foreach ($p['fieldmap'] as $rf => $lf)
95                 $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
96         }
38dc51 97         else if (!empty($p)) {
a97937 98             // read deprecated *_field properties to remain backwards compatible
T 99             foreach ($p as $prop => $value)
100                 if (preg_match('/^(.+)_field$/', $prop, $matches))
101                     $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
b9f9f1 102         }
4f9c83 103
a97937 104         // use fieldmap to advertise supported coltypes to the application
T 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;
1937f4 109                 $this->coltypes[$col] = array('limit' => 1, 'subtypes' => $subtypes);
1148c6 110             }
a97937 111             elseif ($type) {
T 112                 $this->coltypes[$col]['subtypes'][] = $type;
113                 $this->coltypes[$col]['limit']++;
114             }
115             if ($type && !$this->fieldmap[$col])
116                 $this->fieldmap[$col] = $lf;
1148c6 117         }
f76765 118
1937f4 119         // support for composite address
T 120         if ($this->fieldmap['street'] && $this->fieldmap['locality']) {
3ac5cd 121             $this->coltypes['address'] = array('limit' => max(1, $this->coltypes['locality']['limit']), 'subtypes' => $this->coltypes['locality']['subtypes'], 'childs' => array());
T 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             }
1937f4 128         }
19fccd 129         else if ($this->coltypes['address']) {
a97937 130             $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
19fccd 131         }
4f9c83 132
a97937 133         // make sure 'required_fields' is an array
19fccd 134         if (!is_array($this->prop['required_fields'])) {
a97937 135             $this->prop['required_fields'] = (array) $this->prop['required_fields'];
19fccd 136         }
4f9c83 137
19fccd 138         // make sure LDAP_rdn field is required
A 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) {
a97937 144             $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
19fccd 145         }
13db9e 146
A 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         }
f11541 160
f763fb 161         $this->sort_col    = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
A 162         $this->debug       = $debug;
a97937 163         $this->mail_domain = $mail_domain;
8fb04b 164
T 165         // initialize cache
166         $rcmail = rcmail::get_instance();
167         $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
f11541 168
a97937 169         $this->_connect();
736307 170     }
010274 171
736307 172
a97937 173     /**
T 174     * Establish a connection to the LDAP server
175     */
176     private function _connect()
6b603d 177     {
0c2596 178         $RCMAIL = rcmail::get_instance();
f11541 179
a97937 180         if (!function_exists('ldap_connect'))
0c2596 181             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
56651c 182                 'file' => __FILE__, 'line' => __LINE__,
A 183                 'message' => "No ldap support in this installation of PHP"),
184                 true, true);
f11541 185
a97937 186         if (is_resource($this->conn))
T 187             return true;
f11541 188
a97937 189         if (!is_array($this->prop['hosts']))
T 190             $this->prop['hosts'] = array($this->prop['hosts']);
f11541 191
a97937 192         if (empty($this->prop['ldap_version']))
T 193             $this->prop['ldap_version'] = 3;
f11541 194
a97937 195         foreach ($this->prop['hosts'] as $host)
6039aa 196         {
0c2596 197             $host     = idn_to_ascii(rcmail::parse_host($host));
c041d5 198             $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
A 199
8764b6 200             $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
a97937 201
T 202             if ($lc = @ldap_connect($host, $this->prop['port']))
6039aa 203             {
c041d5 204                 if ($this->prop['use_tls'] === true)
a97937 205                     if (!ldap_start_tls($lc))
T 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;
3b4b03 213
T 214                 if (isset($this->prop['referrals']))
215                     ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
a97937 216                 break;
T 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;
c041d5 224         }
A 225
226         if (!is_resource($this->conn)) {
0c2596 227             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
c041d5 228                 'file' => __FILE__, 'line' => __LINE__,
A 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.
0c2596 250             if ($fu = $RCMAIL->get_user_name())
c041d5 251                 list($u, $d) = explode('@', $fu);
A 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']) {
03e520 260                 if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
2d08ec 261                     $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
A 262                 }
263
c041d5 264                 // Search for the dn to use to authenticate
A 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 {
0c2596 289                         rcube::raise_error(array(
c041d5 290                             'code' => 100, 'type' => 'ldap',
A 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;
a97937 324     }
T 325
326
327     /**
4d982d 328      * Bind connection with (SASL-) user and password
A 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      */
9eeb14 336     public function sasl_bind($authc, $pass, $authz=null)
4d982d 337     {
A 338         if (!$this->conn) {
339             return false;
340         }
341
342         if (!function_exists('ldap_sasl_bind')) {
0c2596 343             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
4d982d 344                 'file' => __FILE__, 'line' => __LINE__,
A 345                 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
56651c 346                 true, true);
4d982d 347         }
A 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
0c2596 369         rcube::raise_error(array(
4d982d 370             'code' => ldap_errno($this->conn), 'type' => 'ldap',
A 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     /**
cc90ed 380      * Bind connection with DN and password
A 381      *
382      * @param string Bind DN
383      * @param string Bind password
384      *
385      * @return boolean True on success, False on error
386      */
9eeb14 387     public function bind($dn, $pass)
a97937 388     {
T 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
0c2596 402         rcube::raise_error(array(
56651c 403             'code' => ldap_errno($this->conn), 'type' => 'ldap',
A 404             'file' => __FILE__, 'line' => __LINE__,
405             'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
406             true);
a97937 407
T 408         return false;
409     }
410
411
412     /**
cc90ed 413      * Close connection to LDAP server
A 414      */
a97937 415     function close()
T 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     /**
cc90ed 427      * Returns address book name
A 428      *
429      * @return string Address book name
430      */
431     function get_name()
432     {
433         return $this->prop['name'];
434     }
435
436
437     /**
438753 438      * Set internal sort settings
cc90ed 439      *
438753 440      * @param string $sort_col Sort column
T 441      * @param string $sort_order Sort order
cc90ed 442      */
438753 443     function set_sort_order($sort_col, $sort_order = null)
a97937 444     {
438753 445         if ($this->fieldmap[$sort_col])
T 446             $this->sort_col = $this->fieldmap[$sort_col];
a97937 447     }
T 448
449
450     /**
cc90ed 451      * Save a search string for future listings
A 452      *
453      * @param string $filter Filter string
454      */
a97937 455     function set_search_set($filter)
T 456     {
457         $this->filter = $filter;
458     }
459
460
461     /**
cc90ed 462      * Getter for saved search properties
A 463      *
464      * @return mixed Search properties used by this class
465      */
a97937 466     function get_search_set()
T 467     {
468         return $this->filter;
469     }
470
471
472     /**
cc90ed 473      * Reset all saved results and search parameters
A 474      */
a97937 475     function reset()
T 476     {
477         $this->result = null;
478         $this->ldap_result = null;
479         $this->filter = '';
480     }
481
482
483     /**
cc90ed 484      * List the current set of contact records
A 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      */
a97937 491     function list_records($cols=null, $subset=0)
T 492     {
8fb04b 493         if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
T 494         {
2d3e2b 495             $this->result = new rcube_result_set(0);
T 496             $this->result->searchonly = true;
497             return $this->result;
498         }
b1f084 499
a31482 500         // fetch group members recursively
T 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     /**
9b3311 556      * Get all members of the given group
A 557      *
558      * @param string Group DN
559      * @param array  Group entries (if called recursively)
560      * @return array Accumulated group members
a31482 561      */
b35a0f 562     function list_group_members($dn, $count = false, $entries = null)
a31482 563     {
T 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
337dc5 578         for ($i=0; $i < $entries['count']; $i++)
a31482 579         {
T 580             $entry = $entries[$i];
581
582             if (empty($entry['objectclass']))
583                 continue;
584
b35a0f 585             foreach ((array)$entry['objectclass'] as $objectclass)
a31482 586             {
T 587                 switch (strtolower($objectclass)) {
337dc5 588                     case "group":
a31482 589                     case "groupofnames":
T 590                     case "kolabgroupofnames":
b35a0f 591                         $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
a31482 592                         break;
T 593                     case "groupofuniquenames":
594                     case "kolabgroupofuniquenames":
b35a0f 595                         $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
a31482 596                         break;
T 597                     case "groupofurls":
b35a0f 598                         $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
a31482 599                         break;
T 600                 }
601             }
337dc5 602
fb6cc8 603             if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
T 604               break;
a31482 605         }
T 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      */
b35a0f 618     private function _list_group_members($dn, $entry, $attr, $count)
a31482 619     {
T 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
b35a0f 626         // read these attributes for all members
T 627         $attrib = $count ? array('dn') : array_values($this->fieldmap);
628         $attrib[] = 'objectClass';
629         $attrib[] = 'member';
630         $attrib[] = 'uniqueMember';
631         $attrib[] = 'memberURL';
632
a31482 633         for ($i=0; $i < $entry[$attr]['count']; $i++)
T 634         {
3ed9e8 635             if (empty($entry[$attr][$i]))
T 636                 continue;
637
a31482 638             $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
b35a0f 639                 $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
a31482 640
T 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
b35a0f 649             $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
a31482 650
T 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
b35a0f 663      * @param boolean True if only used for counting
a31482 664      * @return array Accumulated group members
T 665      */
b35a0f 666     private function _list_group_memberurl($dn, $entry, $count)
a31482 667     {
T 668         $group_members = array();
669
670         for ($i=0; $i < $entry['memberurl']['count']; $i++)
8fb04b 671         {
T 672             // extract components from url
a31482 673             if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
T 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
b35a0f 680             $attrib = $count ? array('dn') : array_values($this->fieldmap);
a31482 681             if ($result = @$func($this->conn, $m[1], $filter,
a505dd 682                 $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
A 683             ) {
a31482 684                 $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
T 685             }
a505dd 686             else {
a31482 687                 $this->_debug("S: ".ldap_error($this->conn));
T 688                 return $group_members;
689             }
690
691             $entries = @ldap_get_entries($this->conn, $result);
692             for ($j = 0; $j < $entries['count']; $j++)
693             {
b35a0f 694                 if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
a31482 695                     $group_members = array_merge($group_members, $nested_group_members);
T 696                 else
697                     $group_members[] = $entries[$j];
8fb04b 698             }
a97937 699         }
39cafa 700
a31482 701         return $group_members;
T 702     }
a97937 703
a31482 704     /**
T 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]);
a97937 710     }
T 711
712
713     /**
cc90ed 714      * Search contacts
A 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)
f21a04 718      * @param int     $mode     Matching mode:
A 719      *                          0 - partial (*abc*),
720      *                          1 - strict (=),
721      *                          2 - prefix (abc*)
cc90ed 722      * @param boolean $select   True if results are requested, False if count only
A 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      */
f21a04 728     function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
a97937 729     {
f21a04 730         $mode = intval($mode);
A 731
a97937 732         // special treatment for ID-based search
T 733         if ($fields == 'ID' || $fields == $this->primary_key)
734         {
648674 735             $ids = !is_array($value) ? explode(',', $value) : $value;
a97937 736             $result = new rcube_result_set();
T 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
fc91c1 748         // use VLV pseudo-search for autocompletion
187ff4 749         if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == 'email,name')
fc91c1 750         {
T 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=*)',
7f79e2 760                 array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
fc91c1 761
9b3311 762             $this->result = new rcube_result_set(0);
A 763
6bddd9 764             if (!$this->ldap_result) {
9b3311 765                 $this->_debug("S: ".ldap_error($this->conn));
6bddd9 766                 return $this->result;
A 767             }
9b3311 768
6bddd9 769             $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
9b3311 770
fc91c1 771             // get all entries of this page and post-filter those that really match the query
f21a04 772             $search = mb_strtolower($value);
fc91c1 773             $entries = ldap_get_entries($this->conn, $this->ldap_result);
f21a04 774
fc91c1 775             for ($i = 0; $i < $entries['count']; $i++) {
T 776                 $rec = $this->_ldap2result($entries[$i]);
f21a04 777                 foreach (array('email', 'name') as $f) {
A 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                     }
fc91c1 796                 }
T 797             }
798
799             return $this->result;
800         }
801
e9a9f2 802         // use AND operator for advanced searches
A 803         $filter = is_array($value) ? '(&' : '(|';
f21a04 804         // set wildcards
A 805         $wp = $ws = '';
806         if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
807             $ws = '*';
808             if (!$mode) {
809                 $wp = '*';
810             }
811         }
e9a9f2 812
3cacf9 813         if ($fields == '*')
3e2637 814         {
T 815             // search_fields are required for fulltext search
3cacf9 816             if (empty($this->prop['search_fields']))
3e2637 817             {
T 818                 $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
819                 $this->result = new rcube_result_set();
820                 return $this->result;
821             }
3cacf9 822             if (is_array($this->prop['search_fields']))
A 823             {
e9a9f2 824                 foreach ($this->prop['search_fields'] as $field) {
f21a04 825                     $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
e9a9f2 826                 }
3cacf9 827             }
a97937 828         }
T 829         else
830         {
e9a9f2 831             foreach ((array)$fields as $idx => $field) {
A 832                 $val = is_array($value) ? $value[$idx] : $value;
833                 if ($f = $this->_map_field($field)) {
f21a04 834                     $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
e9a9f2 835                 }
A 836             }
a97937 837         }
T 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     /**
cc90ed 870      * Count number of available contacts in database
A 871      *
872      * @return object rcube_result_set Resultset with values for 'count' and 'first'
873      */
a97937 874     function count()
T 875     {
876         $count = 0;
877         if ($this->conn && $this->ldap_result) {
69ea3a 878             $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
a31482 879         }
T 880         else if ($this->group_id && $this->group_data['dn']) {
b35a0f 881             $count = count($this->list_group_members($this->group_data['dn'], true));
a31482 882         }
T 883         else if ($this->conn) {
a97937 884             // We have a connection but no result set, attempt to get one.
T 885             if (empty($this->filter)) {
886                 // The filter is not set, set it.
887                 $this->filter = $this->prop['filter'];
a31482 888             }
122446 889
A 890             $count = (int) $this->_exec_search(true);
a31482 891         }
a97937 892
T 893         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
894     }
895
896
897     /**
cc90ed 898      * Return the last result set
A 899      *
900      * @return object rcube_result_set Current resultset or NULL if nothing selected yet
901      */
a97937 902     function get_result()
T 903     {
904         return $this->result;
905     }
906
907
908     /**
cc90ed 909      * Get a specific contact record
A 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      */
a97937 916     function get_record($dn, $assoc=false)
T 917     {
13db9e 918         $res = $this->result = null;
A 919
a97937 920         if ($this->conn && $dn)
T 921         {
c3ba0e 922             $dn = self::dn_decode($dn);
a97937 923
T 924             $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
925
13db9e 926             if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
A 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 {
a97937 936                 $this->_debug("S: ".ldap_error($this->conn));
13db9e 937             }
a97937 938
13db9e 939             // Use ldap_list to get subentries like country (c) attribute (#1488123)
A 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             }
a97937 948
13db9e 949             if (!empty($rec)) {
a97937 950                 // Add in the dn for the entry.
T 951                 $rec['dn'] = $dn;
952                 $res = $this->_ldap2result($rec);
13db9e 953                 $this->result = new rcube_result_set();
a97937 954                 $this->result->add($res);
6039aa 955             }
T 956         }
a97937 957
T 958         return $assoc ? $res : $this->result;
f11541 959     }
T 960
961
a97937 962     /**
e84818 963      * Check the given data before saving.
T 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
39cafa 967      * @param boolean Try to fix/complete record automatically
e84818 968      * @return boolean True if input is valid, False if not.
T 969      */
39cafa 970     public function validate(&$save_data, $autofix = false)
e84818 971     {
19fccd 972         // validate e-mail addresses
A 973         if (!parent::validate($save_data, $autofix)) {
974             return false;
975         }
976
e84818 977         // check for name input
T 978         if (empty($save_data['name'])) {
39cafa 979             $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
T 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) {
19fccd 987             if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
39cafa 988                 $missing[$fld] = 1;
T 989             }
990         }
991
992         if ($missing) {
993             // try to complete record automatically
994             if ($autofix) {
19fccd 995                 $sn_field    = $this->fieldmap['surname'];
A 996                 $fn_field    = $this->fieldmap['firstname'];
9336ba 997                 $mail_field  = $this->fieldmap['email'];
A 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']);
19fccd 1002
A 1003                 if ($sn_field && $missing[$sn_field]) {
1004                     $save_data['surname'] = array_pop($name_parts);
1005                     unset($missing[$sn_field]);
39cafa 1006                 }
T 1007
19fccd 1008                 if ($fn_field && $missing[$fn_field]) {
A 1009                     $save_data['firstname'] = array_shift($name_parts);
1010                     unset($missing[$fn_field]);
1011                 }
9336ba 1012
A 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                 }
39cafa 1022             }
T 1023
1024             // TODO: generate message saying which fields are missing
19fccd 1025             if (!empty($missing)) {
A 1026                 $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1027                 return false;
1028             }
e84818 1029         }
cc90ed 1030
19fccd 1031         return true;
e84818 1032     }
T 1033
1034
1035     /**
cc90ed 1036      * Create a new contact record
A 1037      *
1038      * @param array    Hash array with save data
1039      *
1040      * @return encoded record ID on success, False on error
1041      */
a97937 1042     function insert($save_cols)
f11541 1043     {
a97937 1044         // Map out the column names to their LDAP ones to build the new entry.
39cafa 1045         $newentry = $this->_map_data($save_cols);
a97937 1046         $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
T 1047
1048         // Verify that the required fields are set.
6f45fa 1049         $missing = null;
a97937 1050         foreach ($this->prop['required_fields'] as $fld) {
T 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) {
39cafa 1059             $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
a97937 1060             return false;
T 1061         }
1062
1063         // Build the new entries DN.
d1e08f 1064         $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
a97937 1065
13db9e 1066         // Remove attributes that need to be added separately (child objects)
A 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         }
a97937 1076
13db9e 1077         if (!$this->ldap_add($dn, $newentry)) {
a97937 1078             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1079             return false;
13db9e 1080         }
4f9c83 1081
13db9e 1082         foreach ($xfields as $xidx => $xf) {
A 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         }
4f9c83 1091
c3ba0e 1092         $dn = self::dn_encode($dn);
A 1093
a97937 1094         // add new contact to the selected group
5d692b 1095         if ($this->group_id)
c3ba0e 1096             $this->add_to_group($this->group_id, $dn);
4f9c83 1097
c3ba0e 1098         return $dn;
e83f03 1099     }
A 1100
4f9c83 1101
a97937 1102     /**
cc90ed 1103      * Update a specific contact record
A 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      */
a97937 1110     function update($id, $save_cols)
f11541 1111     {
a97937 1112         $record = $this->get_record($id, true);
4aaecb 1113
13db9e 1114         $newdata     = array();
a97937 1115         $replacedata = array();
13db9e 1116         $deletedata  = array();
A 1117         $subdata     = array();
1118         $subdeldata  = array();
1119         $subnewdata  = array();
c3ba0e 1120
d6aafd 1121         $ldap_data = $this->_map_data($save_cols);
13db9e 1122         $old_data  = $record['_raw_attrib'];
1803f8 1123
21a0d9 1124         // special handling of photo col
A 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
a97937 1132         foreach ($this->fieldmap as $col => $fld) {
T 1133             if ($fld) {
13db9e 1134                 $val = $ldap_data[$fld];
A 1135                 $old = $old_data[$fld];
a97937 1136                 // remove empty array values
T 1137                 if (is_array($val))
1138                     $val = array_filter($val);
13db9e 1139                 // $this->_map_data() result and _raw_attrib use different format
A 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);
21a0d9 1143                 }
A 1144                 if (is_array($val) && count($val) == 1 && !is_array($old)) {
1145                     $val = array_pop($val);
13db9e 1146                 }
A 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                 }
21a0d9 1162
a97937 1163                 // The field does exist compare it to the ldap record.
13db9e 1164                 if ($old != $val) {
a97937 1165                     // Changed, but find out how.
13db9e 1166                     if ($old === null) {
a97937 1167                         // Field was not set prior, need to add it.
T 1168                         $newdata[$fld] = $val;
1803f8 1169                     }
T 1170                     else if ($val == '') {
a97937 1171                         // Field supplied is empty, verify that it is not required.
T 1172                         if (!in_array($fld, $this->prop['required_fields'])) {
1173                             // It is not, safe to clear.
1803f8 1174                             $deletedata[$fld] = $old_data[$fld];
T 1175                         }
13db9e 1176                     }
a97937 1177                     else {
T 1178                         // The data was modified, save it out.
1179                         $replacedata[$fld] = $val;
1803f8 1180                     }
a97937 1181                 } // end if
T 1182             } // end if
1183         } // end foreach
13db9e 1184 /*
A 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 */
c3ba0e 1196         $dn = self::dn_decode($id);
a97937 1197
T 1198         // Update the entry as required.
1199         if (!empty($deletedata)) {
1200             // Delete the fields.
13db9e 1201             if (!$this->ldap_mod_del($dn, $deletedata)) {
a97937 1202                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 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)
d1e08f 1212                     .','.$this->base_dn;
a97937 1213                 if ($dn != $newdn) {
T 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)) {
13db9e 1221                 if (!$this->ldap_mod_replace($dn, $replacedata)) {
A 1222                     $this->set_error(self::ERROR_SAVING, 'errorsaving');
a97937 1223                     return false;
T 1224                 }
13db9e 1225             }
a97937 1226         } // end if
13db9e 1227
A 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         }
a97937 1243
T 1244         if (!empty($newdata)) {
1245             // Add the fields.
13db9e 1246             if (!$this->ldap_mod_add($dn, $newdata)) {
a97937 1247                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1248                 return false;
1249             }
1250         } // end if
1251
1252         // Handle RDN change
1253         if (!empty($newrdn)) {
13db9e 1254             if (!$this->ldap_rename($dn, $newrdn, null, true)) {
A 1255                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
a97937 1256                 return false;
T 1257             }
1258
c3ba0e 1259             $dn    = self::dn_encode($dn);
A 1260             $newdn = self::dn_encode($newdn);
1261
a97937 1262             // change the group membership of the contact
13db9e 1263             if ($this->groups) {
c3ba0e 1264                 $group_ids = $this->get_record_groups($dn);
a97937 1265                 foreach ($group_ids as $group_id)
T 1266                 {
c3ba0e 1267                     $this->remove_from_group($group_id, $dn);
A 1268                     $this->add_to_group($group_id, $newdn);
a97937 1269                 }
T 1270             }
c3ba0e 1271
13db9e 1272             $dn = self::dn_decode($newdn);
a97937 1273         }
T 1274
13db9e 1275         // add sub-entries
A 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;
f11541 1288     }
a97937 1289
T 1290
1291     /**
cc90ed 1292      * Mark one or more contact records as deleted
A 1293      *
63fda8 1294      * @param array   Record identifiers
A 1295      * @param boolean Remove record(s) irreversible (unsupported)
cc90ed 1296      *
A 1297      * @return boolean True on success, False on error
1298      */
63fda8 1299     function delete($ids, $force=true)
f11541 1300     {
a97937 1301         if (!is_array($ids)) {
T 1302             // Not an array, break apart the encoded DNs.
0f1fae 1303             $ids = explode(',', $ids);
a97937 1304         } // end if
T 1305
0f1fae 1306         foreach ($ids as $id) {
c3ba0e 1307             $dn = self::dn_decode($id);
13db9e 1308
A 1309             // Need to delete all sub-entries first
1310             if ($this->sub_filter) {
d6eb7c 1311                 if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
13db9e 1312                     foreach ($entries as $entry) {
A 1313                         if (!$this->ldap_delete($entry['dn'])) {
1314                             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1315                             return false;
1316                         }
1317                     }
1318                 }
1319             }
1320
a97937 1321             // Delete the record.
13db9e 1322             if (!$this->ldap_delete($dn)) {
a97937 1323                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1324                 return false;
13db9e 1325             }
a97937 1326
T 1327             // remove contact from all groups where he was member
c3ba0e 1328             if ($this->groups) {
A 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);
a97937 1333                 }
T 1334             }
1335         } // end foreach
1336
0f1fae 1337         return count($ids);
d6eb7c 1338     }
A 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         }
f11541 1355     }
4368a0 1356
A 1357
a97937 1358     /**
cc90ed 1359      * Execute the LDAP search based on the stored credentials
A 1360      */
c1db48 1361     private function _exec_search($count = false)
a97937 1362     {
T 1363         if ($this->ready)
1364         {
1365             $filter = $this->filter ? $this->filter : '(objectclass=*)';
fb6cc8 1366             $function = $this->_scope2func($this->prop['scope'], $ns_function);
a31482 1367
fb6cc8 1368             $this->_debug("C: Search [$filter][dn: $this->base_dn]");
69ea3a 1369
6af7e0 1370             // when using VLV, we get the total count by...
e2a8b4 1371             if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
6af7e0 1372                 // ...either reading numSubOrdinates attribute
a31482 1373                 if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
6af7e0 1374                     $counts = ldap_get_entries($this->conn, $result_count);
T 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                 }
f09c18 1379                 else if (!function_exists('ldap_parse_virtuallist_control'))  // ...or by fetching all records dn and count them
6af7e0 1380                     $this->vlv_count = $this->_exec_search(true);
755189 1381
fb6cc8 1382                 $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
69ea3a 1383             }
100440 1384
c1db48 1385             // only fetch dn for count (should keep the payload low)
T 1386             $attrs = $count ? array('dn') : array_values($this->fieldmap);
d1e08f 1387             if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
a505dd 1388                 $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
A 1389             ) {
f09c18 1390                 // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
a505dd 1391                 if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
A 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                     }
f09c18 1402                 }
T 1403
a505dd 1404                 $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
A 1405                 $this->_debug("S: $entries_count record(s)");
f09c18 1406
a505dd 1407                 return $count ? $entries_count : true;
a97937 1408             }
a505dd 1409             else {
a97937 1410                 $this->_debug("S: ".ldap_error($this->conn));
T 1411             }
1412         }
1413
1414         return false;
69ea3a 1415     }
cc90ed 1416
69ea3a 1417     /**
fb6cc8 1418      * Choose the right PHP function according to scope property
T 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         }
13db9e 1434
fb6cc8 1435         return $function;
T 1436     }
1437
1438     /**
69ea3a 1439      * Set server controls for Virtual List View (paginated listing)
T 1440      */
fc91c1 1441     private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
69ea3a 1442     {
fb6cc8 1443         $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$prop['sort']));
fc91c1 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);
69ea3a 1445
fb6cc8 1446         $sort = (array)$prop['sort'];
T 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)");
69ea3a 1449
T 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;
a97937 1457     }
T 1458
1459
1460     /**
cc90ed 1461      * Converts LDAP entry into an array
A 1462      */
a97937 1463     private function _ldap2result($rec)
T 1464     {
1465         $out = array();
1466
1467         if ($rec['dn'])
c3ba0e 1468             $out[$this->primary_key] = self::dn_encode($rec['dn']);
a97937 1469
T 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;
1803f8 1475
ad8c9d 1476                 list($col, $subtype) = explode(':', $rf);
1803f8 1477                 $out['_raw_attrib'][$lf][$i] = $value;
T 1478
a97937 1479                 if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
T 1480                     $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
ad8c9d 1481                 else if (in_array($col, array('street','zipcode','locality','country','region')))
T 1482                     $out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
a97937 1483                 else if ($rec[$lf]['count'] > 1)
T 1484                     $out[$rf][] = $value;
1485                 else
1486                     $out[$rf] = $value;
1487             }
b1f084 1488
A 1489             // Make sure name fields aren't arrays (#1488108)
1490             if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
1803f8 1491                 $out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
b1f084 1492             }
a97937 1493         }
T 1494
1495         return $out;
1496     }
1497
1498
1499     /**
cc90ed 1500      * Return real field name (from fields map)
A 1501      */
a97937 1502     private function _map_field($field)
T 1503     {
1504         return $this->fieldmap[$field];
1505     }
1506
1507
1508     /**
39cafa 1509      * Convert a record data set into LDAP field attributes
T 1510      */
1511     private function _map_data($save_cols)
1512     {
d6aafd 1513         // flatten composite fields first
T 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
39cafa 1527         $ldap_data = array();
T 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         }
13db9e 1537
39cafa 1538         return $ldap_data;
T 1539     }
1540
1541
1542     /**
cc90ed 1543      * Returns unified attribute name (resolving aliases)
A 1544      */
1545     private static function _attr_name($name)
a97937 1546     {
T 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     /**
cc90ed 1560      * Prints debug info to the log
A 1561      */
a97937 1562     private function _debug($str)
T 1563     {
0c2596 1564         if ($this->debug) {
A 1565             rcmail::write_log('ldap', $str);
1566         }
a97937 1567     }
T 1568
1569
1570     /**
06744d 1571      * Activate/deactivate debug mode
T 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     /**
cc90ed 1583      * Quotes attribute value string
A 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)
a97937 1591     {
T 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     }
f11541 1605
6039aa 1606
T 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         {
a31482 1615             if (($group_cache = $this->cache->get('groups')) === null)
T 1616                 $group_cache = $this->_fetch_groups();
a97937 1617
6039aa 1618             $this->group_id = $group_id;
a31482 1619             $this->group_data = $group_cache[$group_id];
6039aa 1620         }
a97937 1621         else
8fb04b 1622         {
a97937 1623             $this->group_id = 0;
a31482 1624             $this->group_data = null;
8fb04b 1625         }
6039aa 1626     }
T 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     {
a97937 1636         if (!$this->groups)
T 1637             return array();
6039aa 1638
a31482 1639         // use cached list for searching
T 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) {
f21a04 1646             $search = mb_strtolower($search);
a31482 1647             foreach ($group_cache as $group) {
f21a04 1648                 if (strpos(mb_strtolower($group['name']), $search) !== false)
a31482 1649                     $groups[] = $group;
T 1650             }
1651         }
1652         else
1653             $groups = $group_cache;
1654
1655         return array_values($groups);
1656     }
1657
1658     /**
1659      * Fetch groups from server
1660      */
fb6cc8 1661     private function _fetch_groups($vlv_page = 0)
a31482 1662     {
d1e08f 1663         $base_dn = $this->groups_base_dn;
T 1664         $filter = $this->prop['groups']['filter'];
448f81 1665         $name_attr = $this->prop['groups']['name_attr'];
a31482 1666         $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
fb6cc8 1667         $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
T 1668         $sort_attr = $sort_attrs[0];
6039aa 1669
755189 1670         $this->_debug("C: Search [$filter][dn: $base_dn]");
A 1671
fb6cc8 1672         // use vlv to list groups
T 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)));
6039aa 1682         if ($res === false)
T 1683         {
1684             $this->_debug("S: ".ldap_error($this->conn));
1685             return array();
1686         }
755189 1687
6039aa 1688         $ldap_data = ldap_get_entries($this->conn, $res);
755189 1689         $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
6039aa 1690
T 1691         $groups = array();
1692         $group_sortnames = array();
fb6cc8 1693         $group_count = $ldap_data["count"];
T 1694         for ($i=0; $i < $group_count; $i++)
6039aa 1695         {
fb6cc8 1696             $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
a31482 1697             $group_id = self::dn_encode($group_name);
T 1698             $groups[$group_id]['ID'] = $group_id;
1699             $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
1700             $groups[$group_id]['name'] = $group_name;
097dbc 1701             $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
a31482 1702
T 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
f21a04 1709             $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
6039aa 1710         }
fb6cc8 1711
T 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);
8fb04b 1727
T 1728         // cache this
1729         $this->cache->set('groups', $groups);
a97937 1730
6039aa 1731         return $groups;
a31482 1732     }
T 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;
6039aa 1749     }
T 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     {
d1e08f 1759         $base_dn = $this->groups_base_dn;
6039aa 1760         $new_dn = "cn=$group_name,$base_dn";
c3ba0e 1761         $new_gid = self::dn_encode($group_name);
097dbc 1762         $member_attr = $this->get_group_member_attr();
A 1763         $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
6039aa 1764
T 1765         $new_entry = array(
d1e08f 1766             'objectClass' => $this->prop['groups']['object_classes'],
448f81 1767             $name_attr => $group_name,
b4b377 1768             $member_attr => '',
6039aa 1769         );
T 1770
13db9e 1771         if (!$this->ldap_add($new_dn, $new_entry)) {
6039aa 1772             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1773             return false;
1774         }
755189 1775
8fb04b 1776         $this->cache->remove('groups');
755189 1777
6039aa 1778         return array('id' => $new_gid, 'name' => $group_name);
T 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     {
a31482 1789         if (($group_cache = $this->cache->get('groups')) === null)
T 1790             $group_cache = $this->_fetch_groups();
6039aa 1791
d1e08f 1792         $base_dn = $this->groups_base_dn;
8fb04b 1793         $group_name = $group_cache[$group_id]['name'];
6039aa 1794         $del_dn = "cn=$group_name,$base_dn";
755189 1795
13db9e 1796         if (!$this->ldap_delete($del_dn)) {
6039aa 1797             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1798             return false;
1799         }
755189 1800
8fb04b 1801         $this->cache->remove('groups');
755189 1802
6039aa 1803         return true;
T 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
360bd3 1811      * @param string New group identifier (if changed, otherwise don't set)
6039aa 1812      * @return boolean New name on success, false if no data was changed
T 1813      */
15e944 1814     function rename_group($group_id, $new_name, &$new_gid)
6039aa 1815     {
a31482 1816         if (($group_cache = $this->cache->get('groups')) === null)
T 1817             $group_cache = $this->_fetch_groups();
6039aa 1818
d1e08f 1819         $base_dn = $this->groups_base_dn;
8fb04b 1820         $group_name = $group_cache[$group_id]['name'];
6039aa 1821         $old_dn = "cn=$group_name,$base_dn";
T 1822         $new_rdn = "cn=$new_name";
c3ba0e 1823         $new_gid = self::dn_encode($new_name);
6039aa 1824
13db9e 1825         if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
6039aa 1826             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1827             return false;
1828         }
755189 1829
8fb04b 1830         $this->cache->remove('groups');
755189 1831
6039aa 1832         return $new_name;
T 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     {
a31482 1844         if (($group_cache = $this->cache->get('groups')) === null)
T 1845             $group_cache = $this->_fetch_groups();
6039aa 1846
5d692b 1847         if (!is_array($contact_ids))
T 1848             $contact_ids = explode(',', $contact_ids);
1849
f763fb 1850         $base_dn     = $this->groups_base_dn;
8fb04b 1851         $group_name  = $group_cache[$group_id]['name'];
T 1852         $member_attr = $group_cache[$group_id]['member_attr'];
f763fb 1853         $group_dn    = "cn=$group_name,$base_dn";
6039aa 1854
T 1855         $new_attrs = array();
5d692b 1856         foreach ($contact_ids as $id)
f763fb 1857             $new_attrs[$member_attr][] = self::dn_decode($id);
6039aa 1858
13db9e 1859         if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
6039aa 1860             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1861             return 0;
1862         }
755189 1863
8fb04b 1864         $this->cache->remove('groups');
755189 1865
6039aa 1866         return count($new_attrs['member']);
T 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     {
a31482 1878         if (($group_cache = $this->cache->get('groups')) === null)
T 1879             $group_cache = $this->_fetch_groups();
6039aa 1880
f763fb 1881         $base_dn     = $this->groups_base_dn;
8fb04b 1882         $group_name  = $group_cache[$group_id]['name'];
T 1883         $member_attr = $group_cache[$group_id]['member_attr'];
f763fb 1884         $group_dn    = "cn=$group_name,$base_dn";
6039aa 1885
T 1886         $del_attrs = array();
1887         foreach (explode(",", $contact_ids) as $id)
f763fb 1888             $del_attrs[$member_attr][] = self::dn_decode($id);
6039aa 1889
13db9e 1890         if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
6039aa 1891             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1892             return 0;
1893         }
755189 1894
8fb04b 1895         $this->cache->remove('groups');
755189 1896
6039aa 1897         return count($del_attrs['member']);
T 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     {
a97937 1910         if (!$this->groups)
6039aa 1911             return array();
T 1912
f763fb 1913         $base_dn     = $this->groups_base_dn;
A 1914         $contact_dn  = self::dn_decode($contact_id);
097dbc 1915         $name_attr   = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
A 1916         $member_attr = $this->get_group_member_attr();
8fb04b 1917         $add_filter  = '';
T 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('\\' => '\\\\'));
6039aa 1921
755189 1922         $this->_debug("C: Search [$filter][dn: $base_dn]");
A 1923
448f81 1924         $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
6039aa 1925         if ($res === false)
T 1926         {
1927             $this->_debug("S: ".ldap_error($this->conn));
1928             return array();
1929         }
1930         $ldap_data = ldap_get_entries($this->conn, $res);
755189 1931         $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
6039aa 1932
T 1933         $groups = array();
1934         for ($i=0; $i<$ldap_data["count"]; $i++)
1935         {
448f81 1936             $group_name = $ldap_data[$i][$name_attr][0];
c3ba0e 1937             $group_id = self::dn_encode($group_name);
6039aa 1938             $groups[$group_id] = $group_id;
T 1939         }
1940         return $groups;
1941     }
69ea3a 1942
097dbc 1943     /**
A 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
69ea3a 1979
T 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      */
fc91c1 1987     private function _vlv_ber_encode($offset, $rpp, $search = '')
69ea3a 1988     {
T 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
ce53b6 1993         # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
T 1994         # 02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
69ea3a 1995         # a0 = type context-specific/constructed with a length of 06 (6) bytes following
ce53b6 1996         # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
T 1997         # 02 = type integer with 2 bytes following (contentCount):  01 00
fc91c1 1998         
T 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         
69ea3a 2004         # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
T 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
fc91c1 2013         
T 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
69ea3a 2024
fc91c1 2025             $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
cc90ed 2026
fc91c1 2027             // calculate octet length of $ber_val
T 2028             $str = self::_ber_addseq($ber_val, '02') . $str;
69ea3a 2029
fc91c1 2030             // now compute length over $str
T 2031             $str = self::_ber_addseq($str, 'a0');
2032         }
2033         
69ea3a 2034         // now tack on records per page
ce53b6 2035         $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
69ea3a 2036
T 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      *
c3ba0e 2047      * @param array List of cols to sort by
69ea3a 2048      * @return string BER encoded option value
T 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     {
6f3fa9 2073         $len = dechex(strlen($str)/2);
69ea3a 2074         if (strlen($len) % 2 != 0)
T 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     {
6f3fa9 2085         $val = dechex($offset);
69ea3a 2086         $prefix = '';
cc90ed 2087
69ea3a 2088         // check if bit 8 of high byte is 1
T 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      */
c3ba0e 2101     private static function _string2hex($str)
A 2102     {
69ea3a 2103         $hex = '';
T 2104         for ($i=0; $i < strlen($str); $i++)
2105             $hex .= dechex(ord($str[$i]));
2106         return $hex;
2107     }
2108
c3ba0e 2109     /**
A 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     }
13db9e 2135
A 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      */
d6eb7c 2237     protected function ldap_list($dn, $filter, $attrs = array(''))
13db9e 2238     {
A 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
f11541 2262 }