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