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