thomascube
2008-09-25 cefd1d8c913aa81ddce83e9de7f5bfb22aa4b2d9
commit | author | age
d1d2c4 1 <?php
S 2 /*
3  +-----------------------------------------------------------------------+
47124c 4  | program/include/rcube_ldap.php                                        |
d1d2c4 5  |                                                                       |
S 6  | This file is part of the RoundCube Webmail client                     |
47124c 7  | Copyright (C) 2006-2008, RoundCube Dev. - Switzerland                 |
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>                        |
d1d2c4 15  +-----------------------------------------------------------------------+
S 16
17  $Id$
18
19 */
20
6d969b 21
T 22 /**
23  * Model class to access an LDAP address directory
24  *
25  * @package Addressbook
26  */
d1d2c4 27 class rcube_ldap
f11541 28 {
d1d2c4 29   var $conn;
f11541 30   var $prop = array();
T 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;
d1d2c4 55     
f11541 56     foreach ($p as $prop => $value)
T 57       if (preg_match('/^(.+)_field$/', $prop, $matches))
58         $this->fieldmap[$matches[1]] = $value;
4f9c83 59
S 60     $this->sort_col = $p["sort"];
61
f11541 62     $this->connect();
d1d2c4 63   }
S 64
f11541 65
T 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
03b271 80     if (empty($this->prop['ldap_version']))
T 81       $this->prop['ldap_version'] = 3;
82
f11541 83     foreach ($this->prop['hosts'] as $host)
T 84     {
85       if ($lc = @ldap_connect($host, $this->prop['port']))
86       {
70a056 87     if ($this->prop['use_tls']===true)
cd6749 88       if (!ldap_start_tls($lc))
A 89         continue;
90
03b271 91         ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
f11541 92         $this->prop['host'] = $host;
T 93         $this->conn = $lc;
94         break;
95       }
96     }
97     
98     if (is_resource($this->conn))
e3caaf 99     {
f11541 100       $this->ready = true;
4f9c83 101
S 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']))
e3caaf 139         $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']);
T 140     }
f11541 141     else
T 142       raise_error(array('type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true);
4f9c83 143
S 144     // See if the directory is writeable.
145     if ($this->prop['writable']) {
146       $this->readonly = false;
147     } // end if
148
f11541 149   }
T 150
151
152   /**
e3caaf 153    * Bind connection with DN and password
6d969b 154    *
T 155    * @param string Bind DN
156    * @param string Bind password
157    * @return boolean True on success, False on error
f11541 158    */
e3caaf 159   function bind($dn, $pass)
f11541 160   {
736307 161     if (!$this->conn) {
e3caaf 162       return false;
736307 163     }
e3caaf 164     
369efc 165     if (ldap_bind($this->conn, $dn, $pass)) {
e3caaf 166       return true;
736307 167     }
T 168
169     raise_error(array(
e3caaf 170         'code' => ldap_errno($this->conn),
T 171         'type' => 'ldap',
172         'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
736307 173         true);
T 174
e3caaf 175     return false;
T 176   }
f11541 177
T 178
179   /**
180    * Close connection to LDAP server
181    */
182   function close()
183   {
184     if ($this->conn)
6b603d 185     {
f11541 186       @ldap_unbind($this->conn);
6b603d 187       $this->conn = null;
T 188     }
f11541 189   }
T 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    *
6d969b 219    * @param string Filter string
f11541 220    */
T 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
4f9c83 253    * @param  int    Only return this number of records
f11541 254    * @return array  Indexed list of contact records, each a hash array
T 255    */
256   function list_records($cols=null, $subset=0)
257   {
6b603d 258     // add general filter to query
T 259     if (!empty($this->prop['filter']))
260     {
261       $filter = $this->prop['filter'];
262       $this->set_search_set($filter);
263     }
264     
f11541 265     // exec LDAP search if no result resource is stored
T 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);
4f9c83 277
S 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
f11541 282       $entries = ldap_get_entries($this->conn, $this->ldap_result);
4f9c83 283       for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
f11541 284         $this->result->add($this->_ldap2result($entries[$i]));
T 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
6d969b 297    * @return array  Indexed list of contact records and 'count' value
f11541 298    */
3fc00e 299   function search($fields, $value, $strict=false, $select=true)
f11541 300   {
T 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 = '(|';
3fc00e 317     $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
f11541 318     if (is_array($this->prop['search_fields']))
T 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 .= ')';
e3caaf 330     
72c722 331     // avoid double-wildcard if $value is empty
T 332     $filter = preg_replace('/\*+/', '*', $filter);
333     
e3caaf 334     // add general filter to query
T 335     if (!empty($this->prop['filter']))
72c722 336       $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
f11541 337
T 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    *
6d969b 354    * @return object rcube_result_set Resultset with values for 'count' and 'first'
f11541 355    */
T 356   function count()
357   {
358     $count = 0;
4f9c83 359     if ($this->conn && $this->ldap_result) {
f11541 360       $count = ldap_count_entries($this->conn, $this->ldap_result);
4f9c83 361     } // end if
S 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
f11541 373
T 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    *
6d969b 381    * @return object rcube_result_set Current resultset or NULL if nothing selected yet
f11541 382    */
T 383   function get_result()
384   {
385     return $this->result;
386   }
387   
388   
389   /**
390    * Get a specific contact record
391    *
6d969b 392    * @param mixed   Record identifier
T 393    * @param boolean Return as associative array
394    * @return mixed  Hash array or rcube_result_set with all record fields
f11541 395    */
T 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       {
4f9c83 406         // Add in the dn for the entry.
S 407         $rec["dn"] = base64_decode($dn);
f11541 408         $res = $this->_ldap2result($rec);
T 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    *
6d969b 421    * @param array    Hash array with save data
4f9c83 422    * @return encoded record ID on success, False on error
f11541 423    */
T 424   function insert($save_cols)
425   {
4f9c83 426     // Map out the column names to their LDAP ones to build the new entry.
S 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);
f11541 455   }
T 456   
457   
458   /**
459    * Update a specific contact record
460    *
461    * @param mixed Record identifier
6d969b 462    * @param array Hash array with save data
T 463    * @return boolean True on success, False on error
f11541 464    */
T 465   function update($id, $save_cols)
466   {
4f9c83 467     $record = $this->get_record($id, true);
S 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;
f11541 527   }
T 528   
529   
530   /**
531    * Mark one or more contact records as deleted
532    *
533    * @param array  Record identifiers
6d969b 534    * @return boolean True on success, False on error
f11541 535    */
T 536   function delete($ids)
537   {
4f9c83 538     if (!is_array($ids)) {
S 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;
f11541 553   }
T 554
555
556   /**
557    * Execute the LDAP search based on the stored credentials
558    *
6d969b 559    * @access private
f11541 560    */
T 561   function _exec_search()
562   {
563     if ($this->conn && $this->filter)
564     {
565       $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
e3caaf 566       $this->ldap_result = $function($this->conn, $this->prop['base_dn'], $this->filter, array_values($this->fieldmap), 0, 0);
f11541 567       return true;
T 568     }
569     else
570       return false;
571   }
572   
573   
574   /**
6d969b 575    * @access private
f11541 576    */
T 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   /**
6d969b 595    * @access private
f11541 596    */
T 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
47124c 614