thomascube
2011-02-14 3e2637351da9559a4aa420004ac90e9fe30477ef
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                     |
6039aa 7  | Copyright (C) 2006-2011, The Roundcube Dev Team                       |
d1d2c4 8  | Licensed under the GNU GPL                                            |
S 9  |                                                                       |
10  | PURPOSE:                                                              |
f11541 11  |   Interface to an LDAP address directory                              |
d1d2c4 12  |                                                                       |
S 13  +-----------------------------------------------------------------------+
f11541 14  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
6039aa 15  |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
d1d2c4 16  +-----------------------------------------------------------------------+
S 17
18  $Id$
19
20 */
21
6d969b 22
T 23 /**
24  * Model class to access an LDAP address directory
25  *
26  * @package Addressbook
27  */
cc97ea 28 class rcube_ldap extends rcube_addressbook
f11541 29 {
a97937 30     /** public properties */
T 31     public $primary_key = 'ID';
32     public $groups = false;
33     public $readonly = true;
34     public $ready = false;
35     public $group_id = 0;
36     public $list_page = 1;
37     public $page_size = 10;
38     public $coltypes = array();
1148c6 39
a97937 40     /** private properties */
T 41     protected $conn;
42     protected $prop = array();
43     protected $fieldmap = array();
1148c6 44
a97937 45     protected $filter = '';
T 46     protected $result = null;
47     protected $ldap_result = null;
48     protected $sort_col = '';
49     protected $mail_domain = '';
50     protected $debug = false;
6039aa 51
a97937 52     private $group_cache = array();
T 53     private $group_members = array();
1148c6 54
A 55
a97937 56     /**
T 57     * Object constructor
58     *
59     * @param array     LDAP connection properties
60     * @param boolean     Enables debug mode
61     * @param string     Current user mail domain name
62     * @param integer User-ID
63     */
64     function __construct($p, $debug=false, $mail_domain=NULL)
f11541 65     {
a97937 66         $this->prop = $p;
010274 67
a97937 68         // check if groups are configured
T 69         if (is_array($p['groups']))
70             $this->groups = true;
cd6749 71
a97937 72         // fieldmap property is given
T 73         if (is_array($p['fieldmap'])) {
74             foreach ($p['fieldmap'] as $rf => $lf)
75                 $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
76         }
77         else {
78             // read deprecated *_field properties to remain backwards compatible
79             foreach ($p as $prop => $value)
80                 if (preg_match('/^(.+)_field$/', $prop, $matches))
81                     $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
b9f9f1 82         }
4f9c83 83
a97937 84         // use fieldmap to advertise supported coltypes to the application
T 85         foreach ($this->fieldmap as $col => $lf) {
86             list($col, $type) = explode(':', $col);
87             if (!is_array($this->coltypes[$col])) {
88                 $subtypes = $type ? array($type) : null;
89                 $this->coltypes[$col] = array('limit' => 2, 'subtypes' => $subtypes);
1148c6 90             }
a97937 91             elseif ($type) {
T 92                 $this->coltypes[$col]['subtypes'][] = $type;
93                 $this->coltypes[$col]['limit']++;
94             }
95             if ($type && !$this->fieldmap[$col])
96                 $this->fieldmap[$col] = $lf;
1148c6 97         }
f76765 98
a97937 99         if ($this->fieldmap['street'] && $this->fieldmap['locality'])
T 100             $this->coltypes['address'] = array('limit' => 1);
101         else if ($this->coltypes['address'])
102             $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
4f9c83 103
a97937 104         // make sure 'required_fields' is an array
T 105         if (!is_array($this->prop['required_fields']))
106             $this->prop['required_fields'] = (array) $this->prop['required_fields'];
4f9c83 107
a97937 108         foreach ($this->prop['required_fields'] as $key => $val)
T 109             $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
f11541 110
a97937 111         $this->sort_col = $p['sort'];
T 112         $this->debug = $debug;
113         $this->mail_domain = $mail_domain;
f11541 114
a97937 115         $this->_connect();
736307 116     }
010274 117
736307 118
a97937 119     /**
T 120     * Establish a connection to the LDAP server
121     */
122     private function _connect()
6b603d 123     {
a97937 124         global $RCMAIL;
f11541 125
a97937 126         if (!function_exists('ldap_connect'))
T 127             raise_error(array('code' => 100, 'type' => 'ldap',
128             'file' => __FILE__, 'line' => __LINE__,
129             'message' => "No ldap support in this installation of PHP"), true);
f11541 130
a97937 131         if (is_resource($this->conn))
T 132             return true;
f11541 133
a97937 134         if (!is_array($this->prop['hosts']))
T 135             $this->prop['hosts'] = array($this->prop['hosts']);
f11541 136
a97937 137         if (empty($this->prop['ldap_version']))
T 138             $this->prop['ldap_version'] = 3;
f11541 139
a97937 140         foreach ($this->prop['hosts'] as $host)
6039aa 141         {
a97937 142             $host = idn_to_ascii(rcube_parse_host($host));
T 143             $this->_debug("C: Connect [$host".($this->prop['port'] ? ':'.$this->prop['port'] : '')."]");
144
145             if ($lc = @ldap_connect($host, $this->prop['port']))
6039aa 146             {
a97937 147                 if ($this->prop['use_tls']===true)
T 148                     if (!ldap_start_tls($lc))
149                         continue;
150
151                 $this->_debug("S: OK");
152
153                 ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
154                 $this->prop['host'] = $host;
155                 $this->conn = $lc;
156                 break;
157             }
158             $this->_debug("S: NOT OK");
159         }
160
161         if (is_resource($this->conn))
162         {
163             $this->ready = true;
164
165             // User specific access, generate the proper values to use.
166             if ($this->prop['user_specific']) {
167                 // No password set, use the session password
168                 if (empty($this->prop['bind_pass'])) {
169                     $this->prop['bind_pass'] = $RCMAIL->decrypt($_SESSION['password']);
170                 }
171
172                 // Get the pieces needed for variable replacement.
173                 $fu = $RCMAIL->user->get_username();
174                 list($u, $d) = explode('@', $fu);
175                 $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
176
177                 $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
178
179                 if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
180                     // Search for the dn to use to authenticate
181                     $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
182                     $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
183
184                     $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
185
186                     $res = ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
187                     if ($res && ($entry = ldap_first_entry($this->conn, $res))) {
188                         $bind_dn = ldap_get_dn($this->conn, $entry);
189
190                         $this->_debug("S: search returned dn: $bind_dn");
191
192                         if ($bind_dn) {
193                             $this->prop['bind_dn'] = $bind_dn;
194                             $dn = ldap_explode_dn($bind_dn, 1);
195                             $replaces['%dn'] = $dn[0];
196                         }
197                     }
198                 }
199                 // Replace the bind_dn and base_dn variables.
200                 $this->prop['bind_dn'] = strtr($this->prop['bind_dn'], $replaces);
201                 $this->prop['base_dn'] = strtr($this->prop['base_dn'], $replaces);
202             }
203
204             if (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass']))
205                 $this->ready = $this->_bind($this->prop['bind_dn'], $this->prop['bind_pass']);
206         }
207         else
208             raise_error(array('code' => 100, 'type' => 'ldap',
209                 'file' => __FILE__, 'line' => __LINE__,
210                 'message' => "Could not connect to any LDAP server, last tried $host:{$this->prop[port]}"), true);
211
212         // See if the directory is writeable.
213         if ($this->prop['writable']) {
214             $this->readonly = false;
215         } // end if
216     }
217
218
219     /**
220     * Bind connection with DN and password
221     *
222     * @param string Bind DN
223     * @param string Bind password
224     * @return boolean True on success, False on error
225     */
226     private function _bind($dn, $pass)
227     {
228         if (!$this->conn) {
229             return false;
230         }
231
232         $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
233
234         if (@ldap_bind($this->conn, $dn, $pass)) {
235             $this->_debug("S: OK");
236             return true;
237         }
238
239         $this->_debug("S: ".ldap_error($this->conn));
240
241         $error =  array(
242                 'code' => ldap_errno($this->conn), 'type' => 'ldap',
243                 'file' => __FILE__, 'line' => __LINE__,
244                 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn));
245         raise_error($error,true);
246
247         return false;
248     }
249
250
251     /**
252     * Close connection to LDAP server
253     */
254     function close()
255     {
256         if ($this->conn)
257         {
258             $this->_debug("C: Close");
259             ldap_unbind($this->conn);
260             $this->conn = null;
261         }
262     }
263
264
265     /**
266     * Set internal list page
267     *
268     * @param  number  Page number to list
269     * @access public
270     */
271     function set_page($page)
272     {
273         $this->list_page = (int)$page;
274     }
275
276
277     /**
278     * Set internal page size
279     *
280     * @param  number  Number of messages to display on one page
281     * @access public
282     */
283     function set_pagesize($size)
284     {
285         $this->page_size = (int)$size;
286     }
287
288
289     /**
290     * Save a search string for future listings
291     *
292     * @param string Filter string
293     */
294     function set_search_set($filter)
295     {
296         $this->filter = $filter;
297     }
298
299
300     /**
301     * Getter for saved search properties
302     *
303     * @return mixed Search properties used by this class
304     */
305     function get_search_set()
306     {
307         return $this->filter;
308     }
309
310
311     /**
312     * Reset all saved results and search parameters
313     */
314     function reset()
315     {
316         $this->result = null;
317         $this->ldap_result = null;
318         $this->filter = '';
319     }
320
321
322     /**
323     * List the current set of contact records
324     *
325     * @param  array  List of cols to show
326     * @param  int    Only return this number of records
327     * @return array  Indexed list of contact records, each a hash array
328     */
329     function list_records($cols=null, $subset=0)
330     {
331         // add general filter to query
332         if (!empty($this->prop['filter']) && empty($this->filter))
333         {
334             $filter = $this->prop['filter'];
335             $this->set_search_set($filter);
336         }
337
338         // exec LDAP search if no result resource is stored
339         if ($this->conn && !$this->ldap_result)
340             $this->_exec_search();
341
342         // count contacts for this user
343         $this->result = $this->count();
344
345         // we have a search result resource
346         if ($this->ldap_result && $this->result->count > 0)
347         {
348             if ($this->sort_col && $this->prop['scope'] !== 'base')
349                 ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
350
351             $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
352             $last_row = $this->result->first + $this->page_size;
353             $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
354
355             $entries = ldap_get_entries($this->conn, $this->ldap_result);
356             for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
357                 $this->result->add($this->_ldap2result($entries[$i]));
358         }
359
360         // temp hack for filtering group members
361         if ($this->groups and $this->group_id)
362         {
363             $result = new rcube_result_set();
364             while ($record = $this->result->iterate())
365             {
366                 if ($this->group_members[$record['ID']])
367                 {
368                     $result->add($record);
369                     $result->count++;
370                 }
371             }
372             $this->result = $result;
373         }
374
375         return $this->result;
376     }
377
378
379     /**
380     * Search contacts
381     *
382     * @param array   List of fields to search in
383     * @param string  Search value
384     * @param boolean True for strict, False for partial (fuzzy) matching
385     * @param boolean True if results are requested, False if count only
386     * @param boolean (Not used)
387     * @param array   List of fields that cannot be empty
388     * @return array  Indexed list of contact records and 'count' value
389     */
390     function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
391     {
392         // special treatment for ID-based search
393         if ($fields == 'ID' || $fields == $this->primary_key)
394         {
395             $ids = explode(',', $value);
396             $result = new rcube_result_set();
397             foreach ($ids as $id)
398             {
399                 if ($rec = $this->get_record($id, true))
400                 {
401                     $result->add($rec);
402                     $result->count++;
403                 }
404             }
405             return $result;
406         }
407
408         $filter = '(|';
409         $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
3e2637 410         if ($fields != '*')
T 411         {
412             // search_fields are required for fulltext search
413             if (!$this->prop['search_fields'])
414             {
415                 $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
416                 $this->result = new rcube_result_set();
417                 return $this->result;
418             }
419         }
420         
a97937 421         if (is_array($this->prop['search_fields']))
T 422         {
423             foreach ($this->prop['search_fields'] as $k => $field)
424                 $filter .= "($field=$wc" . $this->_quote_string($value) . "$wc)";
425         }
426         else
427         {
428             foreach ((array)$fields as $field)
429                 if ($f = $this->_map_field($field))
430                     $filter .= "($f=$wc" . $this->_quote_string($value) . "$wc)";
431         }
432         $filter .= ')';
433
434         // add required (non empty) fields filter
435         $req_filter = '';
436         foreach ((array)$required as $field)
437             if ($f = $this->_map_field($field))
438                 $req_filter .= "($f=*)";
439
440         if (!empty($req_filter))
441             $filter = '(&' . $req_filter . $filter . ')';
442
443         // avoid double-wildcard if $value is empty
444         $filter = preg_replace('/\*+/', '*', $filter);
445
446         // add general filter to query
447         if (!empty($this->prop['filter']))
448             $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
449
450         // set filter string and execute search
451         $this->set_search_set($filter);
452         $this->_exec_search();
453
454         if ($select)
455             $this->list_records();
456         else
457             $this->result = $this->count();
458
459         return $this->result;
460     }
461
462
463     /**
464     * Count number of available contacts in database
465     *
466     * @return object rcube_result_set Resultset with values for 'count' and 'first'
467     */
468     function count()
469     {
470         $count = 0;
471         if ($this->conn && $this->ldap_result) {
472             $count = ldap_count_entries($this->conn, $this->ldap_result);
473         } // end if
474         elseif ($this->conn) {
475             // We have a connection but no result set, attempt to get one.
476             if (empty($this->filter)) {
477                 // The filter is not set, set it.
478                 $this->filter = $this->prop['filter'];
479             } // end if
480             $this->_exec_search();
481             if ($this->ldap_result) {
482                 $count = ldap_count_entries($this->conn, $this->ldap_result);
483             } // end if
484         } // end else
485
486         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
487     }
488
489
490     /**
491     * Return the last result set
492     *
493     * @return object rcube_result_set Current resultset or NULL if nothing selected yet
494     */
495     function get_result()
496     {
497         return $this->result;
498     }
499
500
501     /**
502     * Get a specific contact record
503     *
504     * @param mixed   Record identifier
505     * @param boolean Return as associative array
506     * @return mixed  Hash array or rcube_result_set with all record fields
507     */
508     function get_record($dn, $assoc=false)
509     {
510         $res = null;
511         if ($this->conn && $dn)
512         {
513             $dn = base64_decode($dn);
514
515             $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
516
517             if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap)))
518                 $entry = ldap_first_entry($this->conn, $this->ldap_result);
519             else
520                 $this->_debug("S: ".ldap_error($this->conn));
521
522             if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
523             {
524                 $this->_debug("S: OK"/* . print_r($rec, true)*/);
525
526                 $rec = array_change_key_case($rec, CASE_LOWER);
527
528                 // Add in the dn for the entry.
529                 $rec['dn'] = $dn;
530                 $res = $this->_ldap2result($rec);
531                 $this->result = new rcube_result_set(1);
532                 $this->result->add($res);
6039aa 533             }
T 534         }
a97937 535
T 536         return $assoc ? $res : $this->result;
f11541 537     }
T 538
539
a97937 540     /**
T 541     * Create a new contact record
542     *
543     * @param array    Hash array with save data
544     * @return encoded record ID on success, False on error
545     */
546     function insert($save_cols)
f11541 547     {
a97937 548         // Map out the column names to their LDAP ones to build the new entry.
T 549         $newentry = array();
550         $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
551         foreach ($this->fieldmap as $col => $fld) {
552             $val = $save_cols[$col];
553             if (is_array($val))
554                 $val = array_filter($val);  // remove empty entries
555             if ($fld && $val) {
556                 // The field does exist, add it to the entry.
557                 $newentry[$fld] = $val;
4f9c83 558             } // end if
a97937 559         } // end foreach
T 560
561         // Verify that the required fields are set.
562         foreach ($this->prop['required_fields'] as $fld) {
563             $missing = null;
564             if (!isset($newentry[$fld])) {
565                 $missing[] = $fld;
566             }
567         }
568
569         // abort process if requiered fields are missing
570         // TODO: generate message saying which fields are missing
571         if ($missing) {
572             $this->set_error(self::ERROR_INCOMPLETE, 'formincomplete');
573             return false;
574         }
575
576         // Build the new entries DN.
577         $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->prop['base_dn'];
578
579         $this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
580
581         $res = ldap_add($this->conn, $dn, $newentry);
582         if ($res === FALSE) {
583             $this->_debug("S: ".ldap_error($this->conn));
584             $this->set_error(self::ERROR_SAVING, 'errorsaving');
585             return false;
4f9c83 586         } // end if
S 587
010274 588         $this->_debug("S: OK");
4f9c83 589
a97937 590         // add new contact to the selected group
T 591         if ($this->groups)
592             $this->add_to_group($this->group_id, base64_encode($dn));
4f9c83 593
a97937 594         return base64_encode($dn);
e83f03 595     }
A 596
4f9c83 597
a97937 598     /**
T 599     * Update a specific contact record
600     *
601     * @param mixed Record identifier
602     * @param array Hash array with save data
603     * @return boolean True on success, False on error
604     */
605     function update($id, $save_cols)
f11541 606     {
a97937 607         $record = $this->get_record($id, true);
T 608         $result = $this->get_result();
609         $record = $result->first();
4aaecb 610
a97937 611         $newdata = array();
T 612         $replacedata = array();
613         $deletedata = array();
614         foreach ($this->fieldmap as $col => $fld) {
615             $val = $save_cols[$col];
616             if ($fld) {
617                 // remove empty array values
618                 if (is_array($val))
619                     $val = array_filter($val);
620                 // The field does exist compare it to the ldap record.
621                 if ($record[$col] != $val) {
622                     // Changed, but find out how.
623                     if (!isset($record[$col])) {
624                         // Field was not set prior, need to add it.
625                         $newdata[$fld] = $val;
626                     } // end if
627                     elseif ($val == '') {
628                         // Field supplied is empty, verify that it is not required.
629                         if (!in_array($fld, $this->prop['required_fields'])) {
630                             // It is not, safe to clear.
631                             $deletedata[$fld] = $record[$col];
632                         } // end if
633                     } // end elseif
634                     else {
635                         // The data was modified, save it out.
636                         $replacedata[$fld] = $val;
637                     } // end else
638                 } // end if
639             } // end if
640         } // end foreach
010274 641
a97937 642         $dn = base64_decode($id);
T 643
644         // Update the entry as required.
645         if (!empty($deletedata)) {
646             // Delete the fields.
647             $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
648             if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
649                 $this->_debug("S: ".ldap_error($this->conn));
650                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
651                 return false;
652             }
653             $this->_debug("S: OK");
654         } // end if
655
656         if (!empty($replacedata)) {
657             // Handle RDN change
658             if ($replacedata[$this->prop['LDAP_rdn']]) {
659                 $newdn = $this->prop['LDAP_rdn'].'='
660                     .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
661                     .','.$this->prop['base_dn'];
662                 if ($dn != $newdn) {
663                     $newrdn = $this->prop['LDAP_rdn'].'='
664                     .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
665                     unset($replacedata[$this->prop['LDAP_rdn']]);
666                 }
667             }
668             // Replace the fields.
669             if (!empty($replacedata)) {
670                 $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
671                 if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
672                     $this->_debug("S: ".ldap_error($this->conn));
673                     return false;
674                 }
675                 $this->_debug("S: OK");
676             } // end if
677         } // end if
678
679         if (!empty($newdata)) {
680             // Add the fields.
681             $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
682             if (!ldap_mod_add($this->conn, $dn, $newdata)) {
683                 $this->_debug("S: ".ldap_error($this->conn));
684                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
685                 return false;
686             }
687             $this->_debug("S: OK");
688         } // end if
689
690         // Handle RDN change
691         if (!empty($newrdn)) {
692             $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
693             if (!ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
694                 $this->_debug("S: ".ldap_error($this->conn));
695                 return false;
696             }
697             $this->_debug("S: OK");
698
699             // change the group membership of the contact
700             if ($this->groups)
701             {
702                 $group_ids = $this->get_record_groups(base64_encode($dn));
703                 foreach ($group_ids as $group_id)
704                 {
705                     $this->remove_from_group($group_id, base64_encode($dn));
706                     $this->add_to_group($group_id, base64_encode($newdn));
707                 }
708             }
709             return base64_encode($newdn);
710         }
711
4aaecb 712         return true;
f11541 713     }
a97937 714
T 715
716     /**
717     * Mark one or more contact records as deleted
718     *
719     * @param array  Record identifiers
720     * @return boolean True on success, False on error
721     */
722     function delete($ids)
f11541 723     {
a97937 724         if (!is_array($ids)) {
T 725             // Not an array, break apart the encoded DNs.
726             $dns = explode(',', $ids);
727         } // end if
728
729         foreach ($dns as $id) {
730             $dn = base64_decode($id);
731             $this->_debug("C: Delete [dn: $dn]");
732             // Delete the record.
733             $res = ldap_delete($this->conn, $dn);
734             if ($res === FALSE) {
735                 $this->_debug("S: ".ldap_error($this->conn));
736                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
737                 return false;
738             } // end if
739             $this->_debug("S: OK");
740
741             // remove contact from all groups where he was member
742             if ($this->groups)
743             {
744                 $group_ids = $this->get_record_groups(base64_encode($dn));
745                 foreach ($group_ids as $group_id)
746                 {
747                     $this->remove_from_group($group_id, base64_encode($dn));
748                 }
749             }
750         } // end foreach
751
752         return count($dns);
f11541 753     }
4368a0 754
A 755
a97937 756     /**
T 757     * Execute the LDAP search based on the stored credentials
758     *
759     * @access private
760     */
761     private function _exec_search()
762     {
763         if ($this->ready)
764         {
765             $filter = $this->filter ? $this->filter : '(objectclass=*)';
766             $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
010274 767
a97937 768             $this->_debug("C: Search [".$filter."]");
100440 769
a97937 770             if ($this->ldap_result = @$function($this->conn, $this->prop['base_dn'], $filter,
T 771                 array_values($this->fieldmap), 0, (int) $this->prop['sizelimit'], (int) $this->prop['timelimit']))
772             {
773                 $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
774                 return true;
775             }
776             else
777             {
778                 $this->_debug("S: ".ldap_error($this->conn));
779             }
780         }
781
782         return false;
783     }
784
785
786     /**
787     * @access private
788     */
789     private function _ldap2result($rec)
790     {
791         $out = array();
792
793         if ($rec['dn'])
794             $out[$this->primary_key] = base64_encode($rec['dn']);
795
796         foreach ($this->fieldmap as $rf => $lf)
797         {
798             for ($i=0; $i < $rec[$lf]['count']; $i++) {
799                 if (!($value = $rec[$lf][$i]))
800                     continue;
801                 if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
802                     $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
803                 else if (in_array($rf, array('street','zipcode','locality','country','region')))
804                     $out['address'][$i][$rf] = $value;
805                 else if ($rec[$lf]['count'] > 1)
806                     $out[$rf][] = $value;
807                 else
808                     $out[$rf] = $value;
809             }
810         }
811
812         return $out;
813     }
814
815
816     /**
817     * @access private
818     */
819     private function _map_field($field)
820     {
821         return $this->fieldmap[$field];
822     }
823
824
825     /**
826     * @access private
827     */
828     private function _attr_name($name)
829     {
830         // list of known attribute aliases
831         $aliases = array(
832             'gn' => 'givenname',
833             'rfc822mailbox' => 'email',
834             'userid' => 'uid',
835             'emailaddress' => 'email',
836             'pkcs9email' => 'email',
837         );
838         return isset($aliases[$name]) ? $aliases[$name] : $name;
839     }
840
841
842     /**
843     * @access private
844     */
845     private function _debug($str)
846     {
847         if ($this->debug)
848             write_log('ldap', $str);
849     }
850
851
852     /**
853     * @static
854     */
855     private function _quote_string($str, $dn=false)
856     {
857         // take firt entry if array given
858         if (is_array($str))
859             $str = reset($str);
860
861         if ($dn)
862             $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
863                 '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
864         else
865             $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
866                 '/'=>'\2f');
867
868         return strtr($str, $replace);
869     }
f11541 870
6039aa 871
T 872     /**
873      * Setter for the current group
874      * (empty, has to be re-implemented by extending class)
875      */
876     function set_group($group_id)
877     {
878         if ($group_id)
879         {
a97937 880             if (!$this->group_cache)
T 881                 $this->list_groups();
882
883             $cache_members = $this->group_cache[$group_id]['members'];
6039aa 884
T 885             $members = array();
a97937 886             for ($i=1; $i<$cache_members["count"]; $i++)
6039aa 887             {
a97937 888                 $members[base64_encode($cache_members[$i])] = 1;
6039aa 889             }
T 890             $this->group_members = $members;
891             $this->group_id = $group_id;
892         }
a97937 893         else
T 894             $this->group_id = 0;
6039aa 895     }
T 896
897     /**
898      * List all active contact groups of this source
899      *
900      * @param string  Optional search string to match group name
901      * @return array  Indexed list of contact groups, each a hash array
902      */
903     function list_groups($search = null)
904     {
a97937 905         if (!$this->groups)
T 906             return array();
6039aa 907
T 908         $base_dn = $this->prop['groups']['base_dn'];
a97937 909         $filter = '(objectClass=groupOfNames)';
6039aa 910
T 911         $res = ldap_search($this->conn, $base_dn, $filter, array('cn','member'));
912         if ($res === false)
913         {
914             $this->_debug("S: ".ldap_error($this->conn));
915             $this->set_error(self::ERROR_SAVING, 'errorsaving');
916             return array();
917         }
918         $ldap_data = ldap_get_entries($this->conn, $res);
919
920         $groups = array();
921         $group_sortnames = array();
922         for ($i=0; $i<$ldap_data["count"]; $i++)
923         {
924             $group_name = $ldap_data[$i]['cn'][0];
925             $group_id = base64_encode($group_name);
926             $groups[$group_id]['ID'] = $group_id;
927             $groups[$group_id]['name'] = $group_name;
928             $groups[$group_id]['members'] = $ldap_data[$i]['member'];
929             $group_sortnames[] = strtolower($group_name);
930         }
931         array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
932         $this->group_cache = $groups;
a97937 933
6039aa 934         return $groups;
T 935     }
936
937     /**
938      * Create a contact group with the given name
939      *
940      * @param string The group name
941      * @return mixed False on error, array with record props in success
942      */
943     function create_group($group_name)
944     {
945         if (!$this->group_cache)
946             $this->list_groups();
947
948         $base_dn = $this->prop['groups']['base_dn'];
949         $new_dn = "cn=$group_name,$base_dn";
950         $new_gid = base64_encode($group_name);
951
952         $new_entry = array(
953             'objectClass' => array('top', 'groupOfNames'),
954             'cn' => $group_name,
955             'member' => '',
956         );
957
958         $res = ldap_add($this->conn, $new_dn, $new_entry);
959         if ($res === false)
960         {
961             $this->_debug("S: ".ldap_error($this->conn));
962             $this->set_error(self::ERROR_SAVING, 'errorsaving');
963             return false;
964         }
965         return array('id' => $new_gid, 'name' => $group_name);
966     }
967
968     /**
969      * Delete the given group and all linked group members
970      *
971      * @param string Group identifier
972      * @return boolean True on success, false if no data was changed
973      */
974     function delete_group($group_id)
975     {
976         if (!$this->group_cache)
977             $this->list_groups();
978
979         $base_dn = $this->prop['groups']['base_dn'];
980         $group_name = $this->group_cache[$group_id]['name'];
981
982         $del_dn = "cn=$group_name,$base_dn";
983         $res = ldap_delete($this->conn, $del_dn);
984         if ($res === false)
985         {
986             $this->_debug("S: ".ldap_error($this->conn));
987             $this->set_error(self::ERROR_SAVING, 'errorsaving');
988             return false;
989         }
990         return true;
991     }
992
993     /**
994      * Rename a specific contact group
995      *
996      * @param string Group identifier
997      * @param string New name to set for this group
998      * @return boolean New name on success, false if no data was changed
999      */
1000     function rename_group($group_id, $new_name)
1001     {
1002         if (!$this->group_cache)
1003             $this->list_groups();
1004
1005         $base_dn = $this->prop['groups']['base_dn'];
1006         $group_name = $this->group_cache[$group_id]['name'];
1007         $old_dn = "cn=$group_name,$base_dn";
1008         $new_rdn = "cn=$new_name";
1009
1010         $res = ldap_rename($this->conn, $old_dn, $new_rdn, NULL, TRUE);
1011         if ($res === false)
1012         {
1013             $this->_debug("S: ".ldap_error($this->conn));
1014             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1015             return false;
1016         }
1017         return $new_name;
1018     }
1019
1020     /**
1021      * Add the given contact records the a certain group
1022      *
1023      * @param string  Group identifier
1024      * @param array   List of contact identifiers to be added
1025      * @return int    Number of contacts added
1026      */
1027     function add_to_group($group_id, $contact_ids)
1028     {
1029         if (!$this->group_cache)
1030             $this->list_groups();
1031
1032         $base_dn = $this->prop['groups']['base_dn'];
1033         $group_name = $this->group_cache[$group_id]['name'];
1034         $group_dn = "cn=$group_name,$base_dn";
1035
1036         $new_attrs = array();
1037         foreach (explode(",", $contact_ids) as $id)
1038             $new_attrs['member'][] = base64_decode($id);
1039
1040         $res = ldap_mod_add($this->conn, $group_dn, $new_attrs);
1041         if ($res === false)
1042         {
1043             $this->_debug("S: ".ldap_error($this->conn));
1044             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1045             return 0;
1046         }
1047         return count($new_attrs['member']);
1048     }
1049
1050     /**
1051      * Remove the given contact records from a certain group
1052      *
1053      * @param string  Group identifier
1054      * @param array   List of contact identifiers to be removed
1055      * @return int    Number of deleted group members
1056      */
1057     function remove_from_group($group_id, $contact_ids)
1058     {
1059         if (!$this->group_cache)
1060             $this->list_groups();
1061
1062         $base_dn = $this->prop['groups']['base_dn'];
1063         $group_name = $this->group_cache[$group_id]['name'];
1064         $group_dn = "cn=$group_name,$base_dn";
1065
1066         $del_attrs = array();
1067         foreach (explode(",", $contact_ids) as $id)
1068             $del_attrs['member'][] = base64_decode($id);
1069
1070         $res = ldap_mod_del($this->conn, $group_dn, $del_attrs);
1071         if ($res === false)
1072         {
1073             $this->_debug("S: ".ldap_error($this->conn));
1074             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1075             return 0;
1076         }
1077         return count($del_attrs['member']);
1078     }
1079
1080     /**
1081      * Get group assignments of a specific contact record
1082      *
1083      * @param mixed Record identifier
1084      *
1085      * @return array List of assigned groups as ID=>Name pairs
1086      * @since 0.5-beta
1087      */
1088     function get_record_groups($contact_id)
1089     {
a97937 1090         if (!$this->groups)
6039aa 1091             return array();
T 1092
1093         $base_dn = $this->prop['groups']['base_dn'];
1094         $contact_dn = base64_decode($contact_id);
1095         $filter = "(member=$contact_dn)";
1096
1097         $res = ldap_search($this->conn, $base_dn, $filter, array('cn'));
1098         if ($res === false)
1099         {
1100             $this->_debug("S: ".ldap_error($this->conn));
1101             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1102             return array();
1103         }
1104         $ldap_data = ldap_get_entries($this->conn, $res);
1105
1106         $groups = array();
1107         for ($i=0; $i<$ldap_data["count"]; $i++)
1108         {
1109             $group_name = $ldap_data[$i]['cn'][0];
1110             $group_id = base64_encode($group_name);
1111             $groups[$group_id] = $group_id;
1112         }
1113         return $groups;
1114     }
f11541 1115 }