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

Last change on this file since 4467 was 4467, checked in by thomasb, 2 years ago

Add groups support for LDAP address books, contributed by Andreas Dick

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