3 +-----------------------------------------------------------------------+
4 | program/include/rcube_ldap.php |
6 | This file is part of the RoundCube Webmail client |
7 | Copyright (C) 2006-2009, RoundCube Dev. - Switzerland |
8 | Licensed under the GNU GPL |
11 | Interface to an LDAP address directory |
13 +-----------------------------------------------------------------------+
14 | Author: Thomas Bruederli <roundcube@gmail.com> |
15 +-----------------------------------------------------------------------+
17 $Id: rcube_ldap.php 2894 2009-08-29 20:56:00Z alec $
23 * Model class to access an LDAP address directory
25 * @package Addressbook
27 class rcube_ldap extends rcube_addressbook
31 var $fieldmap = array();
35 var $ldap_result = null;
38 /** public properties */
39 var $primary_key = 'ID';
49 * @param array LDAP connection properties
50 * @param integer User-ID
52 function __construct($p)
56 foreach ($p as $prop => $value)
57 if (preg_match('/^(.+)_field$/', $prop, $matches))
58 $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
60 foreach ($this->prop['required_fields'] as $key => $val)
61 $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
63 $this->sort_col = $p['sort'];
70 * Establish a connection to the LDAP server
76 if (!function_exists('ldap_connect'))
77 raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "No ldap support in this installation of PHP"), true);
79 if (is_resource($this->conn))
82 if (!is_array($this->prop['hosts']))
83 $this->prop['hosts'] = array($this->prop['hosts']);
85 if (empty($this->prop['ldap_version']))
86 $this->prop['ldap_version'] = 3;
88 foreach ($this->prop['hosts'] as $host)
90 if ($lc = @ldap_connect($host, $this->prop['port']))
92 if ($this->prop['use_tls']===true)
93 if (!ldap_start_tls($lc))
96 ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
97 $this->prop['host'] = $host;
103 if (is_resource($this->conn))
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']);
114 // Get the pieces needed for variable replacement.
115 $fu = $RCMAIL->user->get_username();
116 list($u, $d) = explode('@', $fu);
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);
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']);
128 raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true);
130 // See if the directory is writeable.
131 if ($this->prop['writable']) {
132 $this->readonly = false;
139 * Bind connection with DN and password
141 * @param string Bind DN
142 * @param string Bind password
143 * @return boolean True on success, False on error
145 function bind($dn, $pass)
151 if (@ldap_bind($this->conn, $dn, $pass)) {
156 'code' => ldap_errno($this->conn),
158 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
166 * Close connection to LDAP server
172 ldap_unbind($this->conn);
179 * Set internal list page
181 * @param number Page number to list
184 function set_page($page)
186 $this->list_page = (int)$page;
191 * Set internal page size
193 * @param number Number of messages to display on one page
196 function set_pagesize($size)
198 $this->page_size = (int)$size;
203 * Save a search string for future listings
205 * @param string Filter string
207 function set_search_set($filter)
209 $this->filter = $filter;
214 * Getter for saved search properties
216 * @return mixed Search properties used by this class
218 function get_search_set()
220 return $this->filter;
225 * Reset all saved results and search parameters
229 $this->result = null;
230 $this->ldap_result = null;
236 * List the current set of contact records
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
242 function list_records($cols=null, $subset=0)
244 // add general filter to query
245 if (!empty($this->prop['filter']) && empty($this->filter))
247 $filter = $this->prop['filter'];
248 $this->set_search_set($filter);
251 // exec LDAP search if no result resource is stored
252 if ($this->conn && !$this->ldap_result)
253 $this->_exec_search();
255 // count contacts for this user
256 $this->result = $this->count();
258 // we have a search result resource
259 if ($this->ldap_result && $this->result->count > 0)
261 if ($this->sort_col && $this->prop['scope'] !== 'base')
262 ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
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;
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]));
273 return $this->result;
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
285 function search($fields, $value, $strict=false, $select=true)
287 // special treatment for ID-based search
288 if ($fields == 'ID' || $fields == $this->primary_key)
290 $ids = explode(',', $value);
291 $result = new rcube_result_set();
292 foreach ($ids as $id)
293 if ($rec = $this->get_record($id, true))
303 $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
304 if (is_array($this->prop['search_fields']))
306 foreach ($this->prop['search_fields'] as $k => $field)
307 $filter .= "($field=$wc" . rcube_ldap::quote_string($value) . "$wc)";
311 foreach ((array)$fields as $field)
312 if ($f = $this->_map_field($field))
313 $filter .= "($f=$wc" . rcube_ldap::quote_string($value) . "$wc)";
317 // avoid double-wildcard if $value is empty
318 $filter = preg_replace('/\*+/', '*', $filter);
320 // add general filter to query
321 if (!empty($this->prop['filter']))
322 $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
324 // set filter string and execute search
325 $this->set_search_set($filter);
326 $this->_exec_search();
329 $this->list_records();
331 $this->result = $this->count();
333 return $this->result;
338 * Count number of available contacts in database
340 * @return object rcube_result_set Resultset with values for 'count' and 'first'
345 if ($this->conn && $this->ldap_result) {
346 $count = ldap_count_entries($this->conn, $this->ldap_result);
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'];
354 $this->_exec_search();
355 if ($this->ldap_result) {
356 $count = ldap_count_entries($this->conn, $this->ldap_result);
360 return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
365 * Return the last result set
367 * @return object rcube_result_set Current resultset or NULL if nothing selected yet
369 function get_result()
371 return $this->result;
376 * Get a specific contact record
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
382 function get_record($dn, $assoc=false)
385 if ($this->conn && $dn)
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);
390 if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
392 $rec = array_change_key_case($rec, CASE_LOWER);
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);
402 return $assoc ? $res : $this->result;
407 * Create a new contact record
409 * @param array Hash array with save data
410 * @return encoded record ID on success, False on error
412 function insert($save_cols)
414 // Map out the column names to their LDAP ones to build the new entry.
416 $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
417 foreach ($save_cols as $col => $val) {
418 $fld = $this->_map_field($col);
420 // The field does exist, add it to the entry.
421 $newentry[$fld] = $val;
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')];
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) {
441 return base64_encode($dn);
446 * Update a specific contact record
448 * @param mixed Record identifier
449 * @param array Hash array with save data
450 * @return boolean True on success, False on error
452 function update($id, $save_cols)
454 $record = $this->get_record($id, true);
455 $result = $this->get_result();
456 $record = $result->first();
459 $replacedata = array();
460 $deletedata = array();
461 foreach ($save_cols as $col => $val) {
462 $fld = $this->_map_field($col);
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;
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];
479 // The data was modified, save it out.
480 $replacedata[$fld] = $val;
486 $dn = base64_decode($id);
488 // Update the entry as required.
489 if (!empty($deletedata)) {
490 // Delete the fields.
491 if (!ldap_mod_del($this->conn, $dn, $deletedata))
495 if (!empty($replacedata)) {
497 if ($replacedata[$this->prop['LDAP_rdn']]) {
498 $newdn = $this->prop['LDAP_rdn'].'='.$replacedata[$this->prop['LDAP_rdn']].','.$this->prop['base_dn'];
500 $newrdn = $this->prop['LDAP_rdn'].'='.$replacedata[$this->prop['LDAP_rdn']];
501 unset($replacedata[$this->prop['LDAP_rdn']]);
504 // Replace the fields.
505 if (!empty($replacedata)) {
506 if (!ldap_mod_replace($this->conn, $dn, $replacedata))
511 if (!empty($newdata)) {
513 if (!ldap_mod_add($this->conn, $dn, $newdata))
518 if (!empty($newrdn)) {
519 if (@ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE))
520 return base64_encode($newdn);
528 * Mark one or more contact records as deleted
530 * @param array Record identifiers
531 * @return boolean True on success, False on error
533 function delete($ids)
535 if (!is_array($ids)) {
536 // Not an array, break apart the encoded DNs.
537 $dns = explode(',', $ids);
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) {
554 * Execute the LDAP search based on the stored credentials
558 private function _exec_search()
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);
575 private function _ldap2result($rec)
582 $out[$this->primary_key] = base64_encode($rec['dn']);
584 foreach ($this->fieldmap as $rf => $lf)
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']));
590 $out[$rf] = $rec[$lf][0];
601 private function _map_field($field)
603 return $this->fieldmap[$field];
610 private function _attr_name($name)
612 // list of known attribute aliases
615 'rfc822mailbox' => 'mail',
617 'emailaddress' => 'email',
618 'pkcs9email' => 'email',
620 return isset($aliases[$name]) ? $aliases[$name] : $name;
627 function quote_string($str)
629 return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c'));