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