source: github/program/include/rcube_ldap.php @ b4b31d62

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since b4b31d62 was b4b31d62, checked in by thomascube <thomas@…>, 4 years ago

Suppress repeated ldap warnings + little codestyle fix

  • Property mode set to 100644
File size: 15.9 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-2008, RoundCube Dev. - Switzerland                 |
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 +-----------------------------------------------------------------------+
16
17 $Id$
18
19*/
20
21
22/**
23 * Model class to access an LDAP address directory
24 *
25 * @package Addressbook
26 */
27class rcube_ldap
28{
29  var $conn;
30  var $prop = array();
31  var $fieldmap = array();
32 
33  var $filter = '';
34  var $result = null;
35  var $ldap_result = null;
36  var $sort_col = '';
37 
38  /** public properties */
39  var $primary_key = 'ID';
40  var $readonly = true;
41  var $list_page = 1;
42  var $page_size = 10;
43  var $ready = false;
44 
45 
46  /**
47   * Object constructor
48   *
49   * @param array LDAP connection properties
50   * @param integer User-ID
51   */
52  function __construct($p)
53  {
54    $this->prop = $p;
55   
56    foreach ($p as $prop => $value)
57      if (preg_match('/^(.+)_field$/', $prop, $matches))
58        $this->fieldmap[$matches[1]] = $value;
59
60    $this->sort_col = $p["sort"];
61
62    $this->connect();
63  }
64
65
66  /**
67   * Establish a connection to the LDAP server
68   */
69  function connect()
70  {
71    if (!function_exists('ldap_connect'))
72      raise_error(array('type' => 'ldap', 'message' => "No ldap support in this installation of PHP"), true);
73
74    if (is_resource($this->conn))
75      return true;
76   
77    if (!is_array($this->prop['hosts']))
78      $this->prop['hosts'] = array($this->prop['hosts']);
79
80    if (empty($this->prop['ldap_version']))
81      $this->prop['ldap_version'] = 3;
82
83    foreach ($this->prop['hosts'] as $host)
84    {
85      if ($lc = @ldap_connect($host, $this->prop['port']))
86      {
87        if ($this->prop['use_tls']===true)
88          if (!ldap_start_tls($lc))
89            continue;
90
91        ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
92        $this->prop['host'] = $host;
93        $this->conn = $lc;
94        break;
95      }
96    }
97   
98    if (is_resource($this->conn))
99    {
100      $this->ready = true;
101
102      if ($this->prop["user_specific"]) {
103        // User specific access, generate the proper values to use.
104        global $CONFIG, $RCMAIL;
105        if (empty($this->prop['bind_pass'])) {
106          // No password set, use the users.
107          $this->prop['bind_pass'] = $RCMAIL->decrypt_passwd($_SESSION["password"]);
108        } // end if
109
110        // Get the pieces needed for variable replacement.
111        // See if the logged in username has an "@" in it.
112        if (is_bool(strstr($_SESSION["username"], "@"))) {
113          // It does not, use the global default.
114          $fu = $_SESSION["username"]."@".$CONFIG["username_domain"];
115          $u = $_SESSION["username"];
116          $d = $CONFIG["username_domain"];
117        } // end if
118        else {
119          // It does.
120          $fu = $_SESSION["username"];
121          // Get the pieces needed for username and domain.
122          list($u, $d) = explode("@", $_SESSION["username"]);
123        } # end else
124
125        // Replace the bind_dn variables.
126        $bind_dn = str_replace(array("%fu", "%u", "%d"),
127                               array($fu, $u, $d),
128                               $this->prop['bind_dn']);
129        $this->prop['bind_dn'] = $bind_dn;
130        // Replace the base_dn variables.
131        $base_dn = str_replace(array("%fu", "%u", "%d"),
132                               array($fu, $u, $d),
133                               $this->prop['base_dn']);
134        $this->prop['base_dn'] = $base_dn;
135
136        $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']);
137      } // end if
138      elseif (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass']))
139        $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']);
140    }
141    else
142      raise_error(array('type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true);
143
144    // See if the directory is writeable.
145    if ($this->prop['writable']) {
146      $this->readonly = false;
147    } // end if
148
149  }
150
151
152  /**
153   * Bind connection with DN and password
154   *
155   * @param string Bind DN
156   * @param string Bind password
157   * @return boolean True on success, False on error
158   */
159  function bind($dn, $pass)
160  {
161    if (!$this->conn) {
162      return false;
163    }
164   
165    if (@ldap_bind($this->conn, $dn, $pass)) {
166      return true;
167    }
168
169    raise_error(array(
170        'code' => ldap_errno($this->conn),
171        'type' => 'ldap',
172        'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
173        true);
174
175    return false;
176  }
177
178
179  /**
180   * Close connection to LDAP server
181   */
182  function close()
183  {
184    if ($this->conn)
185    {
186      @ldap_unbind($this->conn);
187      $this->conn = null;
188    }
189  }
190
191
192  /**
193   * Set internal list page
194   *
195   * @param  number  Page number to list
196   * @access public
197   */
198  function set_page($page)
199  {
200    $this->list_page = (int)$page;
201  }
202
203
204  /**
205   * Set internal page size
206   *
207   * @param  number  Number of messages to display on one page
208   * @access public
209   */
210  function set_pagesize($size)
211  {
212    $this->page_size = (int)$size;
213  }
214
215
216  /**
217   * Save a search string for future listings
218   *
219   * @param string Filter string
220   */
221  function set_search_set($filter)
222  {
223    $this->filter = $filter;
224  }
225 
226 
227  /**
228   * Getter for saved search properties
229   *
230   * @return mixed Search properties used by this class
231   */
232  function get_search_set()
233  {
234    return $this->filter;
235  }
236
237
238  /**
239   * Reset all saved results and search parameters
240   */
241  function reset()
242  {
243    $this->result = null;
244    $this->ldap_result = null;
245    $this->filter = '';
246  }
247 
248 
249  /**
250   * List the current set of contact records
251   *
252   * @param  array  List of cols to show
253   * @param  int    Only return this number of records
254   * @return array  Indexed list of contact records, each a hash array
255   */
256  function list_records($cols=null, $subset=0)
257  {
258    // add general filter to query
259    if (!empty($this->prop['filter']))
260    {
261      $filter = $this->prop['filter'];
262      $this->set_search_set($filter);
263    }
264   
265    // exec LDAP search if no result resource is stored
266    if ($this->conn && !$this->ldap_result)
267      $this->_exec_search();
268   
269    // count contacts for this user
270    $this->result = $this->count();
271   
272    // we have a search result resource
273    if ($this->ldap_result && $this->result->count > 0)
274    {
275      if ($this->sort_col && $this->prop['scope'] !== "base")
276        @ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
277
278      $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
279      $last_row = $this->result->first + $this->page_size;
280      $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
281
282      $entries = ldap_get_entries($this->conn, $this->ldap_result);
283      for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
284        $this->result->add($this->_ldap2result($entries[$i]));
285    }
286
287    return $this->result;
288  }
289
290
291  /**
292   * Search contacts
293   *
294   * @param array   List of fields to search in
295   * @param string  Search value
296   * @param boolean True if results are requested, False if count only
297   * @return array  Indexed list of contact records and 'count' value
298   */
299  function search($fields, $value, $strict=false, $select=true)
300  {
301    // special treatment for ID-based search
302    if ($fields == 'ID' || $fields == $this->primary_key)
303    {
304      $ids = explode(',', $value);
305      $result = new rcube_result_set();
306      foreach ($ids as $id)
307        if ($rec = $this->get_record($id, true))
308        {
309          $result->add($rec);
310          $result->count++;
311        }
312     
313      return $result;
314    }
315   
316    $filter = '(|';
317    $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
318    if (is_array($this->prop['search_fields']))
319    {
320      foreach ($this->prop['search_fields'] as $k => $field)
321        $filter .= "($field=$wc" . rcube_ldap::quote_string($value) . "$wc)";
322    }
323    else
324    {
325      foreach ((array)$fields as $field)
326        if ($f = $this->_map_field($field))
327          $filter .= "($f=$wc" . rcube_ldap::quote_string($value) . "$wc)";
328    }
329    $filter .= ')';
330   
331    // avoid double-wildcard if $value is empty
332    $filter = preg_replace('/\*+/', '*', $filter);
333   
334    // add general filter to query
335    if (!empty($this->prop['filter']))
336      $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
337
338    // set filter string and execute search
339    $this->set_search_set($filter);
340    $this->_exec_search();
341   
342    if ($select)
343      $this->list_records();
344    else
345      $this->result = $this->count();
346   
347    return $this->result; 
348  }
349
350
351  /**
352   * Count number of available contacts in database
353   *
354   * @return object rcube_result_set Resultset with values for 'count' and 'first'
355   */
356  function count()
357  {
358    $count = 0;
359    if ($this->conn && $this->ldap_result) {
360      $count = ldap_count_entries($this->conn, $this->ldap_result);
361    } // end if
362    elseif ($this->conn) {
363      // We have a connection but no result set, attempt to get one.
364      if (empty($this->filter)) {
365        // The filter is not set, set it.
366        $this->filter = $this->prop['filter'];
367      } // end if
368      $this->_exec_search();
369      if ($this->ldap_result) {
370        $count = ldap_count_entries($this->conn, $this->ldap_result);
371      } // end if
372    } // end else
373
374    return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
375  }
376
377
378  /**
379   * Return the last result set
380   *
381   * @return object rcube_result_set Current resultset or NULL if nothing selected yet
382   */
383  function get_result()
384  {
385    return $this->result;
386  }
387 
388 
389  /**
390   * Get a specific contact record
391   *
392   * @param mixed   Record identifier
393   * @param boolean Return as associative array
394   * @return mixed  Hash array or rcube_result_set with all record fields
395   */
396  function get_record($dn, $assoc=false)
397  {
398    $res = null;
399    if ($this->conn && $dn)
400    {
401      $this->ldap_result = @ldap_read($this->conn, base64_decode($dn), "(objectclass=*)", array_values($this->fieldmap));
402      $entry = @ldap_first_entry($this->conn, $this->ldap_result);
403     
404      if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
405      {
406        // Add in the dn for the entry.
407        $rec["dn"] = base64_decode($dn);
408        $res = $this->_ldap2result($rec);
409        $this->result = new rcube_result_set(1);
410        $this->result->add($res);
411      }
412    }
413
414    return $assoc ? $res : $this->result;
415  }
416 
417 
418  /**
419   * Create a new contact record
420   *
421   * @param array    Hash array with save data
422   * @return encoded record ID on success, False on error
423   */
424  function insert($save_cols)
425  {
426    // Map out the column names to their LDAP ones to build the new entry.
427    $newentry = array();
428    $newentry["objectClass"] = $this->prop["LDAP_Object_Classes"];
429    foreach ($save_cols as $col => $val) {
430      $fld = "";
431      $fld = $this->_map_field($col);
432      if ($fld != "") {
433        // The field does exist, add it to the entry.
434        $newentry[$fld] = $val;
435      } // end if
436    } // end foreach
437
438    // Verify that the required fields are set.
439    // We know that the email address is required as a default of rcube, so
440    // we will default its value into any unfilled required fields.
441    foreach ($this->prop["required_fields"] as $fld) {
442      if (!isset($newentry[$fld])) {
443        $newentry[$fld] = $newentry[$this->_map_field("email")];
444      } // end if
445    } // end foreach
446
447    // Build the new entries DN.
448    $dn = $this->prop["LDAP_rdn"]."=".$newentry[$this->prop["LDAP_rdn"]].",".$this->prop['base_dn'];
449    $res = @ldap_add($this->conn, $dn, $newentry);
450    if ($res === FALSE) {
451      return false;
452    } // end if
453
454    return base64_encode($dn);
455  }
456 
457 
458  /**
459   * Update a specific contact record
460   *
461   * @param mixed Record identifier
462   * @param array Hash array with save data
463   * @return boolean True on success, False on error
464   */
465  function update($id, $save_cols)
466  {
467    $record = $this->get_record($id, true);
468    $result = $this->get_result();
469    $record = $result->first();
470
471    $newdata = array();
472    $replacedata = array();
473    $deletedata = array();
474    foreach ($save_cols as $col => $val) {
475      $fld = "";
476      $fld = $this->_map_field($col);
477      if ($fld != "") {
478        // The field does exist compare it to the ldap record.
479        if ($record[$col] != $val) {
480          // Changed, but find out how.
481          if (!isset($record[$col])) {
482            // Field was not set prior, need to add it.
483            $newdata[$fld] = $val;
484          } // end if
485          elseif ($val == "") {
486            // Field supplied is empty, verify that it is not required.
487            if (!in_array($fld, $this->prop["required_fields"])) {
488              // It is not, safe to clear.
489              $deletedata[$fld] = $record[$col];
490            } // end if
491          } // end elseif
492          else {
493            // The data was modified, save it out.
494            $replacedata[$fld] = $val;
495          } // end else
496        } // end if
497      } // end if
498    } // end foreach
499
500    // Update the entry as required.
501    $dn = base64_decode($id);
502    if (!empty($deletedata)) {
503      // Delete the fields.
504      $res = @ldap_mod_del($this->conn, $dn, $deletedata);
505      if ($res === FALSE) {
506        return false;
507      } // end if
508    } // end if
509
510    if (!empty($replacedata)) {
511      // Replace the fields.
512      $res = @ldap_mod_replace($this->conn, $dn, $replacedata);
513      if ($res === FALSE) {
514        return false;
515      } // end if
516    } // end if
517
518    if (!empty($newdata)) {
519      // Add the fields.
520      $res = @ldap_mod_add($this->conn, $dn, $newdata);
521      if ($res === FALSE) {
522        return false;
523      } // end if
524    } // end if
525
526    return true;
527  }
528 
529 
530  /**
531   * Mark one or more contact records as deleted
532   *
533   * @param array  Record identifiers
534   * @return boolean True on success, False on error
535   */
536  function delete($ids)
537  {
538    if (!is_array($ids)) {
539      // Not an array, break apart the encoded DNs.
540      $dns = explode(",", $ids);
541    } // end if
542
543    foreach ($dns as $id) {
544      $dn = base64_decode($id);
545      // Delete the record.
546      $res = @ldap_delete($this->conn, $dn);
547      if ($res === FALSE) {
548        return false;
549      } // end if
550    } // end foreach
551
552    return true;
553  }
554
555
556  /**
557   * Execute the LDAP search based on the stored credentials
558   *
559   * @access private
560   */
561  function _exec_search()
562  {
563    if ($this->ready && $this->filter)
564    {
565      $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
566      $this->ldap_result = $function($this->conn, $this->prop['base_dn'], $this->filter, array_values($this->fieldmap), 0, 0);
567      return true;
568    }
569    else
570      return false;
571  }
572 
573 
574  /**
575   * @access private
576   */
577  function _ldap2result($rec)
578  {
579    $out = array();
580   
581    if ($rec['dn'])
582      $out[$this->primary_key] = base64_encode($rec['dn']);
583   
584    foreach ($this->fieldmap as $rf => $lf)
585    {
586      if ($rec[$lf]['count'])
587        $out[$rf] = $rec[$lf][0];
588    }
589   
590    return $out;
591  }
592 
593 
594  /**
595   * @access private
596   */
597  function _map_field($field)
598  {
599    return $this->fieldmap[$field];
600  }
601 
602 
603  /**
604   * @static
605   */
606  function quote_string($str)
607  {
608    return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c'));
609  }
610
611
612}
613
614
Note: See TracBrowser for help on using the repository browser.