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 2237 2009-01-17 01:55:39Z till $
23 * Model class to access an LDAP address directory
25 * @package 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]] = $value;
60 $this->sort_col = $p["sort"];
67 * Establish a connection to the LDAP server
73 if (!function_exists('ldap_connect'))
74 raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "No ldap support in this installation of PHP"), true);
76 if (is_resource($this->conn))
79 if (!is_array($this->prop['hosts']))
80 $this->prop['hosts'] = array($this->prop['hosts']);
82 if (empty($this->prop['ldap_version']))
83 $this->prop['ldap_version'] = 3;
85 foreach ($this->prop['hosts'] as $host)
87 if ($lc = @ldap_connect($host, $this->prop['port']))
89 if ($this->prop['use_tls']===true)
90 if (!ldap_start_tls($lc))
93 ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
94 $this->prop['host'] = $host;
100 if (is_resource($this->conn))
104 // User specific access, generate the proper values to use.
105 if ($this->prop["user_specific"]) {
106 // No password set, use the session password
107 if (empty($this->prop['bind_pass'])) {
108 $this->prop['bind_pass'] = $RCMAIL->decrypt_passwd($_SESSION["password"]);
111 // Get the pieces needed for variable replacement.
112 $fu = $RCMAIL->user->get_username();
113 list($u, $d) = explode('@', $fu);
115 // Replace the bind_dn and base_dn variables.
116 $replaces = array('%fu' => $fu, '%u' => $u, '%d' => $d);
117 $this->prop['bind_dn'] = strtr($this->prop['bind_dn'], $replaces);
118 $this->prop['base_dn'] = strtr($this->prop['base_dn'], $replaces);
121 if (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass']))
122 $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']);
125 raise_error(array('code' => 100, 'type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true);
127 // See if the directory is writeable.
128 if ($this->prop['writable']) {
129 $this->readonly = false;
136 * Bind connection with DN and password
138 * @param string Bind DN
139 * @param string Bind password
140 * @return boolean True on success, False on error
142 function bind($dn, $pass)
148 if (@ldap_bind($this->conn, $dn, $pass)) {
153 'code' => ldap_errno($this->conn),
155 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
163 * Close connection to LDAP server
169 @ldap_unbind($this->conn);
176 * Set internal list page
178 * @param number Page number to list
181 function set_page($page)
183 $this->list_page = (int)$page;
188 * Set internal page size
190 * @param number Number of messages to display on one page
193 function set_pagesize($size)
195 $this->page_size = (int)$size;
200 * Save a search string for future listings
202 * @param string Filter string
204 function set_search_set($filter)
206 $this->filter = $filter;
211 * Getter for saved search properties
213 * @return mixed Search properties used by this class
215 function get_search_set()
217 return $this->filter;
222 * Reset all saved results and search parameters
226 $this->result = null;
227 $this->ldap_result = null;
233 * List the current set of contact records
235 * @param array List of cols to show
236 * @param int Only return this number of records
237 * @return array Indexed list of contact records, each a hash array
239 function list_records($cols=null, $subset=0)
241 // add general filter to query
242 if (!empty($this->prop['filter']) && empty($this->filter))
244 $filter = $this->prop['filter'];
245 $this->set_search_set($filter);
248 // exec LDAP search if no result resource is stored
249 if ($this->conn && !$this->ldap_result)
250 $this->_exec_search();
252 // count contacts for this user
253 $this->result = $this->count();
255 // we have a search result resource
256 if ($this->ldap_result && $this->result->count > 0)
258 if ($this->sort_col && $this->prop['scope'] !== "base")
259 @ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
261 $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
262 $last_row = $this->result->first + $this->page_size;
263 $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
265 $entries = ldap_get_entries($this->conn, $this->ldap_result);
266 for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
267 $this->result->add($this->_ldap2result($entries[$i]));
270 return $this->result;
277 * @param array List of fields to search in
278 * @param string Search value
279 * @param boolean True if results are requested, False if count only
280 * @return array Indexed list of contact records and 'count' value
282 function search($fields, $value, $strict=false, $select=true)
284 // special treatment for ID-based search
285 if ($fields == 'ID' || $fields == $this->primary_key)
287 $ids = explode(',', $value);
288 $result = new rcube_result_set();
289 foreach ($ids as $id)
290 if ($rec = $this->get_record($id, true))
300 $wc = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
301 if (is_array($this->prop['search_fields']))
303 foreach ($this->prop['search_fields'] as $k => $field)
304 $filter .= "($field=$wc" . rcube_ldap::quote_string($value) . "$wc)";
308 foreach ((array)$fields as $field)
309 if ($f = $this->_map_field($field))
310 $filter .= "($f=$wc" . rcube_ldap::quote_string($value) . "$wc)";
314 // avoid double-wildcard if $value is empty
315 $filter = preg_replace('/\*+/', '*', $filter);
317 // add general filter to query
318 if (!empty($this->prop['filter']))
319 $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
321 // set filter string and execute search
322 $this->set_search_set($filter);
323 $this->_exec_search();
326 $this->list_records();
328 $this->result = $this->count();
330 return $this->result;
335 * Count number of available contacts in database
337 * @return object rcube_result_set Resultset with values for 'count' and 'first'
342 if ($this->conn && $this->ldap_result) {
343 $count = ldap_count_entries($this->conn, $this->ldap_result);
345 elseif ($this->conn) {
346 // We have a connection but no result set, attempt to get one.
347 if (empty($this->filter)) {
348 // The filter is not set, set it.
349 $this->filter = $this->prop['filter'];
351 $this->_exec_search();
352 if ($this->ldap_result) {
353 $count = ldap_count_entries($this->conn, $this->ldap_result);
357 return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
362 * Return the last result set
364 * @return object rcube_result_set Current resultset or NULL if nothing selected yet
366 function get_result()
368 return $this->result;
373 * Get a specific contact record
375 * @param mixed Record identifier
376 * @param boolean Return as associative array
377 * @return mixed Hash array or rcube_result_set with all record fields
379 function get_record($dn, $assoc=false)
382 if ($this->conn && $dn)
384 $this->ldap_result = @ldap_read($this->conn, base64_decode($dn), "(objectclass=*)", array_values($this->fieldmap));
385 $entry = @ldap_first_entry($this->conn, $this->ldap_result);
387 if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
389 // Add in the dn for the entry.
390 $rec["dn"] = base64_decode($dn);
391 $res = $this->_ldap2result($rec);
392 $this->result = new rcube_result_set(1);
393 $this->result->add($res);
397 return $assoc ? $res : $this->result;
402 * Create a new contact record
404 * @param array Hash array with save data
405 * @return encoded record ID on success, False on error
407 function insert($save_cols)
409 // Map out the column names to their LDAP ones to build the new entry.
411 $newentry["objectClass"] = $this->prop["LDAP_Object_Classes"];
412 foreach ($save_cols as $col => $val) {
414 $fld = $this->_map_field($col);
416 // The field does exist, add it to the entry.
417 $newentry[$fld] = $val;
421 // Verify that the required fields are set.
422 // We know that the email address is required as a default of rcube, so
423 // we will default its value into any unfilled required fields.
424 foreach ($this->prop["required_fields"] as $fld) {
425 if (!isset($newentry[$fld])) {
426 $newentry[$fld] = $newentry[$this->_map_field("email")];
430 // Build the new entries DN.
431 $dn = $this->prop["LDAP_rdn"]."=".$newentry[$this->prop["LDAP_rdn"]].",".$this->prop['base_dn'];
432 $res = @ldap_add($this->conn, $dn, $newentry);
433 if ($res === FALSE) {
437 return base64_encode($dn);
442 * Update a specific contact record
444 * @param mixed Record identifier
445 * @param array Hash array with save data
446 * @return boolean True on success, False on error
448 function update($id, $save_cols)
450 $record = $this->get_record($id, true);
451 $result = $this->get_result();
452 $record = $result->first();
455 $replacedata = array();
456 $deletedata = array();
457 foreach ($save_cols as $col => $val) {
459 $fld = $this->_map_field($col);
461 // The field does exist compare it to the ldap record.
462 if ($record[$col] != $val) {
463 // Changed, but find out how.
464 if (!isset($record[$col])) {
465 // Field was not set prior, need to add it.
466 $newdata[$fld] = $val;
468 elseif ($val == "") {
469 // Field supplied is empty, verify that it is not required.
470 if (!in_array($fld, $this->prop["required_fields"])) {
471 // It is not, safe to clear.
472 $deletedata[$fld] = $record[$col];
476 // The data was modified, save it out.
477 $replacedata[$fld] = $val;
483 // Update the entry as required.
484 $dn = base64_decode($id);
485 if (!empty($deletedata)) {
486 // Delete the fields.
487 $res = @ldap_mod_del($this->conn, $dn, $deletedata);
488 if ($res === FALSE) {
493 if (!empty($replacedata)) {
494 // Replace the fields.
495 $res = @ldap_mod_replace($this->conn, $dn, $replacedata);
496 if ($res === FALSE) {
501 if (!empty($newdata)) {
503 $res = @ldap_mod_add($this->conn, $dn, $newdata);
504 if ($res === FALSE) {
514 * Mark one or more contact records as deleted
516 * @param array Record identifiers
517 * @return boolean True on success, False on error
519 function delete($ids)
521 if (!is_array($ids)) {
522 // Not an array, break apart the encoded DNs.
523 $dns = explode(",", $ids);
526 foreach ($dns as $id) {
527 $dn = base64_decode($id);
528 // Delete the record.
529 $res = @ldap_delete($this->conn, $dn);
530 if ($res === FALSE) {
540 * Execute the LDAP search based on the stored credentials
544 function _exec_search()
546 if ($this->ready && $this->filter)
548 $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
549 $this->ldap_result = $function($this->conn, $this->prop['base_dn'], $this->filter, array_values($this->fieldmap), 0, 0);
560 function _ldap2result($rec)
565 $out[$this->primary_key] = base64_encode($rec['dn']);
567 foreach ($this->fieldmap as $rf => $lf)
569 if ($rec[$lf]['count'])
570 $out[$rf] = $rec[$lf][0];
580 function _map_field($field)
582 return $this->fieldmap[$field];
589 function quote_string($str)
591 return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c'));