| | |
| | | | program/include/rcube_ldap.php | |
| | | | | |
| | | | This file is part of the RoundCube Webmail client | |
| | | | Copyright (C) 2006-2008, RoundCube Dev. - Switzerland | |
| | | | Copyright (C) 2006-2009, RoundCube Dev. - Switzerland | |
| | | | Licensed under the GNU GPL | |
| | | | | |
| | | | PURPOSE: | |
| | |
| | | * |
| | | * @package Addressbook |
| | | */ |
| | | class rcube_ldap |
| | | class rcube_ldap extends rcube_addressbook |
| | | { |
| | | var $conn; |
| | | var $prop = array(); |
| | |
| | | var $result = null; |
| | | var $ldap_result = null; |
| | | var $sort_col = ''; |
| | | var $mail_domain = ''; |
| | | var $debug = false; |
| | | |
| | | /** public properties */ |
| | | var $primary_key = 'ID'; |
| | |
| | | /** |
| | | * Object constructor |
| | | * |
| | | * @param array LDAP connection properties |
| | | * @param array LDAP connection properties |
| | | * @param boolean Enables debug mode |
| | | * @param string Current user mail domain name |
| | | * @param integer User-ID |
| | | */ |
| | | function __construct($p) |
| | | function __construct($p, $debug=false, $mail_domain=NULL) |
| | | { |
| | | $this->prop = $p; |
| | | |
| | | |
| | | foreach ($p as $prop => $value) |
| | | if (preg_match('/^(.+)_field$/', $prop, $matches)) |
| | | $this->fieldmap[$matches[1]] = $value; |
| | | |
| | | $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value)); |
| | | |
| | | // make sure 'required_fields' is an array |
| | | if (!is_array($this->prop['required_fields'])) |
| | | $this->prop['required_fields'] = (array) $this->prop['required_fields']; |
| | | |
| | | foreach ($this->prop['required_fields'] as $key => $val) |
| | | $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val)); |
| | | |
| | | $this->sort_col = $p['sort']; |
| | | $this->debug = $debug; |
| | | $this->mail_domain = $mail_domain; |
| | | |
| | | $this->connect(); |
| | | } |
| | | |
| | | /** |
| | | * PHP 4 object constructor |
| | | * |
| | | * @see rcube_ldap::__construct() |
| | | */ |
| | | function rcube_ldap($p) |
| | | { |
| | | $this->__construct($p); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Establish a connection to the LDAP server |
| | | */ |
| | | function connect() |
| | | { |
| | | global $RCMAIL; |
| | | |
| | | if (!function_exists('ldap_connect')) |
| | | raise_error(array('type' => 'ldap', 'message' => "No ldap support in this installation of PHP"), true); |
| | | raise_error(array('code' => 100, 'type' => 'ldap', |
| | | 'file' => __FILE__, 'line' => __LINE__, |
| | | 'message' => "No ldap support in this installation of PHP"), true); |
| | | |
| | | if (is_resource($this->conn)) |
| | | return true; |
| | |
| | | |
| | | foreach ($this->prop['hosts'] as $host) |
| | | { |
| | | $this->_debug("C: Connect [$host".($this->prop['port'] ? ':'.$this->prop['port'] : '')."]"); |
| | | |
| | | if ($lc = @ldap_connect($host, $this->prop['port'])) |
| | | { |
| | | if ($this->prop['use_tls']===true) |
| | | if (!ldap_start_tls($lc)) |
| | | continue; |
| | | |
| | | $this->_debug("S: OK"); |
| | | |
| | | ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']); |
| | | $this->prop['host'] = $host; |
| | | $this->conn = $lc; |
| | | break; |
| | | } |
| | | $this->_debug("S: NOT OK"); |
| | | } |
| | | |
| | | if (is_resource($this->conn)) |
| | | { |
| | | $this->ready = true; |
| | | |
| | | // User specific access, generate the proper values to use. |
| | | if ($this->prop['user_specific']) { |
| | | // No password set, use the session password |
| | | if (empty($this->prop['bind_pass'])) { |
| | | $this->prop['bind_pass'] = $RCMAIL->decrypt($_SESSION['password']); |
| | | } |
| | | |
| | | // Get the pieces needed for variable replacement. |
| | | $fu = $RCMAIL->user->get_username(); |
| | | list($u, $d) = explode('@', $fu); |
| | | |
| | | // Replace the bind_dn and base_dn variables. |
| | | $replaces = array('%fu' => $fu, '%u' => $u, '%d' => $d); |
| | | $this->prop['bind_dn'] = strtr($this->prop['bind_dn'], $replaces); |
| | | $this->prop['base_dn'] = strtr($this->prop['base_dn'], $replaces); |
| | | } |
| | | |
| | | if (!empty($this->prop['bind_dn']) && !empty($this->prop['bind_pass'])) |
| | | $this->ready = $this->bind($this->prop['bind_dn'], $this->prop['bind_pass']); |
| | | } |
| | | else |
| | | raise_error(array('type' => 'ldap', 'message' => "Could not connect to any LDAP server, tried $host:{$this->prop[port]} last"), true); |
| | | raise_error(array('code' => 100, 'type' => 'ldap', |
| | | 'file' => __FILE__, 'line' => __LINE__, |
| | | 'message' => "Could not connect to any LDAP server, last tried $host:{$this->prop[port]}"), true); |
| | | |
| | | // See if the directory is writeable. |
| | | if ($this->prop['writable']) { |
| | | $this->readonly = false; |
| | | } // end if |
| | | |
| | | } |
| | | |
| | | |
| | |
| | | return false; |
| | | } |
| | | |
| | | if (ldap_bind($this->conn, $dn, $pass)) { |
| | | $this->_debug("C: Bind [dn: $dn] [pass: $pass]"); |
| | | |
| | | if (@ldap_bind($this->conn, $dn, $pass)) { |
| | | $this->_debug("S: OK"); |
| | | return true; |
| | | } |
| | | |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | |
| | | raise_error(array( |
| | | 'code' => ldap_errno($this->conn), |
| | | 'type' => 'ldap', |
| | | 'code' => ldap_errno($this->conn), 'type' => 'ldap', |
| | | 'file' => __FILE__, 'line' => __LINE__, |
| | | 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)), |
| | | true); |
| | | |
| | |
| | | { |
| | | if ($this->conn) |
| | | { |
| | | @ldap_unbind($this->conn); |
| | | $this->_debug("C: Close"); |
| | | ldap_unbind($this->conn); |
| | | $this->conn = null; |
| | | } |
| | | } |
| | |
| | | * List the current set of contact records |
| | | * |
| | | * @param array List of cols to show |
| | | * @param int Only return this number of records (not implemented) |
| | | * @param int Only return this number of records |
| | | * @return array Indexed list of contact records, each a hash array |
| | | */ |
| | | function list_records($cols=null, $subset=0) |
| | | { |
| | | // add general filter to query |
| | | if (!empty($this->prop['filter'])) |
| | | if (!empty($this->prop['filter']) && empty($this->filter)) |
| | | { |
| | | $filter = $this->prop['filter']; |
| | | $this->set_search_set($filter); |
| | | } |
| | | |
| | | |
| | | // exec LDAP search if no result resource is stored |
| | | if ($this->conn && !$this->ldap_result) |
| | | $this->_exec_search(); |
| | | |
| | | // count contacts for this user |
| | | $this->result = $this->count(); |
| | | |
| | | |
| | | // we have a search result resource |
| | | if ($this->ldap_result && $this->result->count > 0) |
| | | { |
| | | if ($this->sort_col && $this->prop['scope'] !== "base") |
| | | @ldap_sort($this->conn, $this->ldap_result, $this->sort_col); |
| | | |
| | | if ($this->sort_col && $this->prop['scope'] !== 'base') |
| | | ldap_sort($this->conn, $this->ldap_result, $this->sort_col); |
| | | |
| | | $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first; |
| | | $last_row = $this->result->first + $this->page_size; |
| | | $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row; |
| | | |
| | | $entries = ldap_get_entries($this->conn, $this->ldap_result); |
| | | for ($i = $this->result->first; $i < min($entries['count'], $this->result->first + $this->page_size); $i++) |
| | | for ($i = $start_row; $i < min($entries['count'], $last_row); $i++) |
| | | $this->result->add($this->_ldap2result($entries[$i])); |
| | | } |
| | | |
| | |
| | | * |
| | | * @param array List of fields to search in |
| | | * @param string Search value |
| | | * @param boolean True for strict, False for partial (fuzzy) matching |
| | | * @param boolean True if results are requested, False if count only |
| | | * @return array Indexed list of contact records and 'count' value |
| | | */ |
| | |
| | | function count() |
| | | { |
| | | $count = 0; |
| | | if ($this->conn && $this->ldap_result) |
| | | if ($this->conn && $this->ldap_result) { |
| | | $count = ldap_count_entries($this->conn, $this->ldap_result); |
| | | } // end if |
| | | elseif ($this->conn) { |
| | | // We have a connection but no result set, attempt to get one. |
| | | if (empty($this->filter)) { |
| | | // The filter is not set, set it. |
| | | $this->filter = $this->prop['filter']; |
| | | } // end if |
| | | $this->_exec_search(); |
| | | if ($this->ldap_result) { |
| | | $count = ldap_count_entries($this->conn, $this->ldap_result); |
| | | } // end if |
| | | } // end else |
| | | |
| | | return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); |
| | | } |
| | |
| | | $res = null; |
| | | if ($this->conn && $dn) |
| | | { |
| | | $this->ldap_result = @ldap_read($this->conn, base64_decode($dn), "(objectclass=*)", array_values($this->fieldmap)); |
| | | $entry = @ldap_first_entry($this->conn, $this->ldap_result); |
| | | |
| | | $dn = base64_decode($dn); |
| | | |
| | | $this->_debug("C: Read [dn: $dn] [(objectclass=*)]"); |
| | | |
| | | if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) |
| | | $entry = ldap_first_entry($this->conn, $this->ldap_result); |
| | | else |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | |
| | | if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) |
| | | { |
| | | $this->_debug("S: OK"); |
| | | |
| | | $rec = array_change_key_case($rec, CASE_LOWER); |
| | | |
| | | // Add in the dn for the entry. |
| | | $rec['dn'] = $dn; |
| | | $res = $this->_ldap2result($rec); |
| | | $this->result = new rcube_result_set(1); |
| | | $this->result->add($res); |
| | |
| | | * Create a new contact record |
| | | * |
| | | * @param array Hash array with save data |
| | | * @return boolean The create record ID on success, False on error |
| | | * @return encoded record ID on success, False on error |
| | | */ |
| | | function insert($save_cols) |
| | | { |
| | | // TODO |
| | | return false; |
| | | // Map out the column names to their LDAP ones to build the new entry. |
| | | $newentry = array(); |
| | | $newentry['objectClass'] = $this->prop['LDAP_Object_Classes']; |
| | | foreach ($save_cols as $col => $val) { |
| | | $fld = $this->_map_field($col); |
| | | if ($fld && $val) { |
| | | // The field does exist, add it to the entry. |
| | | $newentry[$fld] = $val; |
| | | } // end if |
| | | } // end foreach |
| | | |
| | | // Verify that the required fields are set. |
| | | // We know that the email address is required as a default of rcube, so |
| | | // we will default its value into any unfilled required fields. |
| | | foreach ($this->prop['required_fields'] as $fld) { |
| | | if (!isset($newentry[$fld])) { |
| | | $newentry[$fld] = $newentry[$this->_map_field('email')]; |
| | | } // end if |
| | | } // end foreach |
| | | |
| | | // Build the new entries DN. |
| | | $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap::quote_string($newentry[$this->prop['LDAP_rdn']], true) |
| | | .','.$this->prop['base_dn']; |
| | | |
| | | $this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true)); |
| | | |
| | | $res = ldap_add($this->conn, $dn, $newentry); |
| | | if ($res === FALSE) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | return false; |
| | | } // end if |
| | | |
| | | $this->_debug("S: OK"); |
| | | |
| | | return base64_encode($dn); |
| | | } |
| | | |
| | | |
| | |
| | | */ |
| | | function update($id, $save_cols) |
| | | { |
| | | // TODO |
| | | return false; |
| | | $record = $this->get_record($id, true); |
| | | $result = $this->get_result(); |
| | | $record = $result->first(); |
| | | |
| | | $newdata = array(); |
| | | $replacedata = array(); |
| | | $deletedata = array(); |
| | | foreach ($save_cols as $col => $val) { |
| | | $fld = $this->_map_field($col); |
| | | if ($fld) { |
| | | // The field does exist compare it to the ldap record. |
| | | if ($record[$col] != $val) { |
| | | // Changed, but find out how. |
| | | if (!isset($record[$col])) { |
| | | // Field was not set prior, need to add it. |
| | | $newdata[$fld] = $val; |
| | | } // end if |
| | | elseif ($val == '') { |
| | | // Field supplied is empty, verify that it is not required. |
| | | if (!in_array($fld, $this->prop['required_fields'])) { |
| | | // It is not, safe to clear. |
| | | $deletedata[$fld] = $record[$col]; |
| | | } // end if |
| | | } // end elseif |
| | | else { |
| | | // The data was modified, save it out. |
| | | $replacedata[$fld] = $val; |
| | | } // end else |
| | | } // end if |
| | | } // end if |
| | | } // end foreach |
| | | |
| | | $dn = base64_decode($id); |
| | | |
| | | // Update the entry as required. |
| | | if (!empty($deletedata)) { |
| | | // Delete the fields. |
| | | $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true)); |
| | | if (!ldap_mod_del($this->conn, $dn, $deletedata)) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | return false; |
| | | } |
| | | $this->_debug("S: OK"); |
| | | } // end if |
| | | |
| | | if (!empty($replacedata)) { |
| | | // Handle RDN change |
| | | if ($replacedata[$this->prop['LDAP_rdn']]) { |
| | | $newdn = $this->prop['LDAP_rdn'].'=' |
| | | .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true) |
| | | .','.$this->prop['base_dn']; |
| | | if ($dn != $newdn) { |
| | | $newrdn = $this->prop['LDAP_rdn'].'=' |
| | | .rcube_ldap::quote_string($replacedata[$this->prop['LDAP_rdn']], true); |
| | | unset($replacedata[$this->prop['LDAP_rdn']]); |
| | | } |
| | | } |
| | | // Replace the fields. |
| | | if (!empty($replacedata)) { |
| | | $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true)); |
| | | if (!ldap_mod_replace($this->conn, $dn, $replacedata)) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | return false; |
| | | } |
| | | $this->_debug("S: OK"); |
| | | } // end if |
| | | } // end if |
| | | |
| | | if (!empty($newdata)) { |
| | | // Add the fields. |
| | | $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true)); |
| | | if (!ldap_mod_add($this->conn, $dn, $newdata)) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | return false; |
| | | } |
| | | $this->_debug("S: OK"); |
| | | } // end if |
| | | |
| | | // Handle RDN change |
| | | if (!empty($newrdn)) { |
| | | $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]"); |
| | | if (@ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | return base64_encode($newdn); |
| | | } |
| | | $this->_debug("S: OK"); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | |
| | |
| | | */ |
| | | function delete($ids) |
| | | { |
| | | // TODO |
| | | return false; |
| | | if (!is_array($ids)) { |
| | | // Not an array, break apart the encoded DNs. |
| | | $dns = explode(',', $ids); |
| | | } // end if |
| | | |
| | | foreach ($dns as $id) { |
| | | $dn = base64_decode($id); |
| | | $this->_debug("C: Delete [dn: $dn]"); |
| | | // Delete the record. |
| | | $res = ldap_delete($this->conn, $dn); |
| | | if ($res === FALSE) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | return false; |
| | | } // end if |
| | | $this->_debug("S: OK"); |
| | | } // end foreach |
| | | |
| | | return count($dns); |
| | | } |
| | | |
| | | |
| | |
| | | * |
| | | * @access private |
| | | */ |
| | | function _exec_search() |
| | | private function _exec_search() |
| | | { |
| | | if ($this->conn && $this->filter) |
| | | if ($this->ready) |
| | | { |
| | | $filter = $this->filter ? $this->filter : '(objectclass=*)'; |
| | | $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list'); |
| | | $this->ldap_result = $function($this->conn, $this->prop['base_dn'], $this->filter, array_values($this->fieldmap), 0, 0); |
| | | return true; |
| | | |
| | | $this->_debug("C: Search [".$filter."]"); |
| | | |
| | | if ($this->ldap_result = @$function($this->conn, $this->prop['base_dn'], $filter, |
| | | array_values($this->fieldmap), 0, (int) $this->prop['sizelimit'], (int) $this->prop['timelimit']) |
| | | ) { |
| | | $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)"); |
| | | return true; |
| | | } else |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | } |
| | | else |
| | | return false; |
| | | |
| | | return false; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @access private |
| | | */ |
| | | function _ldap2result($rec) |
| | | private function _ldap2result($rec) |
| | | { |
| | | global $RCMAIL; |
| | | |
| | | $out = array(); |
| | | |
| | | if ($rec['dn']) |
| | |
| | | |
| | | foreach ($this->fieldmap as $rf => $lf) |
| | | { |
| | | if ($rec[$lf]['count']) |
| | | $out[$rf] = $rec[$lf][0]; |
| | | if ($rec[$lf]['count']) { |
| | | if ($rf == 'email' && $this->mail_domain && !strpos($rec[$lf][0], '@')) |
| | | $out[$rf] = sprintf('%s@%s', $rec[$lf][0], $this->mail_domain); |
| | | else |
| | | $out[$rf] = $rec[$lf][0]; |
| | | } |
| | | } |
| | | |
| | | return $out; |
| | |
| | | /** |
| | | * @access private |
| | | */ |
| | | function _map_field($field) |
| | | private function _map_field($field) |
| | | { |
| | | return $this->fieldmap[$field]; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @static |
| | | * @access private |
| | | */ |
| | | function quote_string($str) |
| | | private function _attr_name($name) |
| | | { |
| | | return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c')); |
| | | // list of known attribute aliases |
| | | $aliases = array( |
| | | 'gn' => 'givenname', |
| | | 'rfc822mailbox' => 'email', |
| | | 'userid' => 'uid', |
| | | 'emailaddress' => 'email', |
| | | 'pkcs9email' => 'email', |
| | | ); |
| | | return isset($aliases[$name]) ? $aliases[$name] : $name; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @access private |
| | | */ |
| | | private function _debug($str) |
| | | { |
| | | if ($this->debug) |
| | | write_log('ldap', $str); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @static |
| | | */ |
| | | function quote_string($str, $dn=false) |
| | | { |
| | | if ($dn) |
| | | $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c', |
| | | '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23'); |
| | | else |
| | | $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c', |
| | | '/'=>'\2f'); |
| | | |
| | | return strtr($str, $replace); |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | ?> |