ldap_addressbook-master/LDAPDirectory.inc
LDAPDirectory.inc
<?php //Dear emacs, please make this buffer -*- php -*- /** * @file * An abstraction that represents a directory (actually the equivalent of a * Distinguished Name, or DN) in a LDAP server. * * Copyright (C) 2006 Andre dos Anjos * * This file is part of the ldap_addresbook module for Drupal. * * The ldap_addressbook module is free software; you can redistribute it * and/or modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 59 Temple * Place, Suite 330, Boston, MA 02111-1307 USA **/ require_once('Format.inc'); /** * This abstraction represents an LDAP user. */ class DirUser { var $dn; //The DN of the user in the LDAP database var $passwd; //The password for this user /** * Constructor * * @param $dn The DN of the user in an LDAP database * @param $passwd That user's password */ function DirUser($dn, $passwd) { $this->dn = $dn; $this->passwd = $passwd; } } /** * This abstraction represents a directory sitting in an LDAP server. It allows * the user to search, add, modify and browse the LDAP server. It is * drupal-aware and will output messages directly to drupal in case of * problems. */ class LDAPDirectory { var $host; //The host where this directory is located at var $port; //The port number on the host that the server is bound to var $dir; //The DN name of directory in the LDAP server var $proto; //The protocol version to use for communication /** * Constructor * * @param $host The host where this directory is located at * @param $port The port number on the host that the server is bound to * @param $dir The DN name of directory in the LDAP server * @param $proto The protocol version to use */ function LDAPDirectory($host, $port, $dir, $proto) { $this->host = $host; $this->port = (int)$port; $this->dir = $dir; $this->proto = (int)$proto; if ($this->proto != 2 and $this->proto != 3) { drupal_set_message (t("Protocol version can only be 2 or 3. I cannot use '$proto'"), 'error'); } } /** * Tests the connection to the LDAP server. Returns true if everything goes * fine or false otherwise. * * @see ldap_connect() */ function _test_connection() { $connection = ldap_connect($this->host, $this->port); if (!$connection) return FALSE; ldap_close($connection); return TRUE; } /** * Connects and binds to the LDAP server, using the given user or anonymously * if no user is given. * * @param $user The user with which to bind to the LDAP server, or FALSE, in * which case I'll try to bind anonymously. * * @return The connection, if every thing goes fine otherwise FALSE * * @see ldap_connect() * @see ldap_bind() * @see DirUser */ function _connect($user=FALSE) { $connection = ldap_connect($this->host, $this->port); if (!$connection) { drupal_set_message (t("Cannot connect to LDAP server at ldap://$this->host:$this->port"), 'error'); return FALSE; } if (!ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, $this->proto)) { drupal_set_message(t("Cannot set the LDAP protocol to version $this->prot"), 'error'); $this->_disconnect($connection); return FALSE; } if (!$user) { //go annonymously $bind = ldap_bind($connection); if (!$bind) { drupal_set_message(t("Cannot bind to the LDAP server (ldap://$this->host:$this->port) anonymously"), 'error'); $this->_disconnect($connection); return FALSE; } } else { //bind with the user $bind = ldap_bind($connection, $user->dn, $user->passwd); if (!$bind) { drupal_set_message(t("Cannot bind to the LDAP server (ldap://$this->host:$this->port) as user (DN) '$user->dn'"), 'error'); $this->_disconnect($connection); return FALSE; } } //if you get here, it is safe to return a connection return $connection; } /** * Disconnection is the easiest to implement:) * * @see ldap_unbind() */ function _disconnect($connection) { //ldap_unbind() is said to be more "correct" than ldap_close() if ($connection) ldap_unbind($connection); } /** * Add a new entry to this directory * * @param $dn The distinguished name of the entry to add * @param $info The card information for the new entry, as expected by the * PHP standard function ldap_add() * @param $user The user with writing permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * * @return <tt>TRUE</tt> if the operation is successful or <tt>FALSE</tt> * otherwise. * * @see ldap_add() * @see DirUser */ function add($dn, $info, $user=FALSE) { $connection = $this->_connect($user); if (!$connection) return FALSE; $result = ldap_add($connection, $dn, $info); $this->_disconnect($connection); if (!$result) { //could not perform the addition drupal_set_message (t("I could not add the LDAP addressbook entry '$dn'."), 'error'); return FALSE; } return TRUE; } /** * Add a new contact to this directory * * @param $info The card information for the new entry, as expected by the * PHP standard function ldap_add(), excluding the 'cn' entry that will be * used to create the distinguished name. * @param $user The user with writing permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * * @return <tt>TRUE</tt> if the operation is successful or <tt>FALSE</tt> * otherwise. * * @see ldap_add() * @see DirUser */ function add_person($info, $user=FALSE) { //check for the givenName and sn if (!array_key_exists('givenname', $info)) { drupal_set_message (t("To add an entry, I need the attribute ")._lat('givenname'), 'error'); return FALSE; } if (!array_key_exists('sn', $info)) { drupal_set_message (t("To add an entry, I need the attribute ")._lat('sn'), 'error'); return FALSE; } //build the DN $cn = $info['givenname'].' '.$info['sn']; $info['cn'] = $cn; $dn = "cn=$cn,$this->dir"; //makes sure we get the right ObjectClass $info['ObjectClass'] = array('top', 'inetOrgPerson'); return $this->add($dn, $info, $user); } /** * This will remove an entry from this directory. * * @param $user The user with writing permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * @param $cn The CN value of the entry you want to remove * * @return <tt>TRUE</tt> if the operation is successful or <tt>FALSE</tt> * otherwise. * * @see ldap_delete() * @see DirUser */ function delete ($cn, $user=FALSE) { //build the DN $dn = "cn=$cn,$this->dir"; $connection = $this->_connect($user); if (!$connection) return FALSE; $result = ldap_delete($connection, $dn); $this->_disconnect($connection); if (!$result) { //could not perform the deletion drupal_set_message (t("I could not delete the LDAP addressbook entry '$cn'."), 'error'); return FALSE; } return TRUE; } /** * This will modify an entry in this directory. * * @param $info The card information for the new entry, as expected by the * PHP standard function ldap_add(), excluding the 'cn' entry. * @param $user The user with writing permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * * @return <tt>TRUE</tt> if the operation is successful or <tt>FALSE</tt> * otherwise. * * @see ldap_modify() * @see DirUser */ function modify ($info, $user=FALSE) { //check for the givenName and sn //trigger_error(implode(',',array_keys($info))); if (!array_key_exists('givenname', $info)) { drupal_set_message (t("To modify an entry, I need the attribute ")._lat('givenname'), 'error'); return FALSE; } if (!array_key_exists('sn', $info)) { drupal_set_message (t("To modify an entry, I need the attribute ")._lat('sn'), 'error'); return FALSE; } $cn = $info['givenname'].' '.$info['sn']; $connection = $this->_connect($user); if (!$connection) return FALSE; //check if we still have cn = givenName + sn if ($info['cn'] != $cn) { $oldcn = $info['cn']; drupal_set_message(t("Replacing entry $oldcn by $cn")); $info = $this->get_one($oldcn, $user); foreach ($info as $key => $field) $info[$key] = $field; $info['cn'] = $cn; $retval = TRUE; if (! $this->delete($oldcn, $user)) $retval = FALSE; if (! $this->add_person($info, $user)) $retval = FALSE; $this->_disconnect($connection); return $retval; } //build the DN $dn = "cn=$cn,$this->dir"; $result = ldap_modify($connection, $dn, $info); $this->_disconnect($connection); if (!$result) { //could not perform the addition drupal_set_message (t("I could not modify the LDAP addressbook entry '$cn'."), 'error'); return FALSE; } drupal_set_message(t("Successfuly changed LDAP addressbook entry $cn")); return TRUE; } /** * Implements a modified (binary-safe) function to return the entries, so it * correctly return things like jpeg photos and other binary entries. * * @param $connection The LDAP connection, properly bound * @param $search_result The results after ldap_search() has been called. * * @return An array that contains the entries organized as in the * result. Every entry is an array with the LDAP field properties. No * "count" fields are available at this version of the result array */ function _format_entries($connection, $search_result) { $i=0; $retval = array(); for ($entry=ldap_first_entry($connection, $search_result); $entry != FALSE; $entry=ldap_next_entry($connection, $entry)) { $attributes = ldap_get_attributes($connection, $entry); $j = 0; for($j; $j<$attributes['count']; $j++) { $values = ldap_get_values_len($connection, $entry, $attributes[$j]); $retval[$i][strtolower($attributes[$j])] = $values; } $i++; } return $retval; } /** * Do a search in the current directory for all entries. * * @param $filter The filter should conform to the string representation for * search filters as defined in RFC 2254. If this variable is not provided, * the default filter (objectClass=*) is used instead. * @param $user The user with reading permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * @param $sort The sorting criteria to apply, in reverse order, as an * array. The keys here are checked against $fields bellow, if that is set, * so don't worry too much. * @param $fields An array containing the LDAP fields you want to * retrieve. The default is to retrive all the available fields for every * entry. * @param $sub If the scope of the search should be sub(tree). Otherwise, if * unset or FALSE, the search will be realized only in the current * directory. Not on subdirectories. * * @return An array where every entry is the CN value of the LDAP entry and * the contents of every entry is an array with the entries fields. An empty * array means the search has not found any matches. A value of FALSE as * return value indicates there were problems. */ function search_any($filter='', $user=FALSE, $sort=array('cn'), $fields=NULL, $sub=FALSE) { if (empty($filter)) $filter='(objectClass=*)'; $connection = $this->_connect($user); if (!$connection) return FALSE; //set the scope $result = FALSE; if ($sub) $result = ldap_search($connection, $this->dir, $filter, $fields); else $result = ldap_list($connection, $this->dir, $filter, $fields); if (!$result) { drupal_set_message(t('Cannot search the LDAP directory '.$this->dir), 'error'); $this->_disconnect($connection); return FALSE; } //the sorting in reverse order foreach ($sort as $s) { if (is_null($fields)) ldap_sort($connection, $result, $s); else if (in_array($s, $fields)) ldap_sort($connection, $result, $s); } //get into a PHPified version of the answer $info = $this->_format_entries($connection, $result); $this->_disconnect($connection); return $info; } /** * Do a search in the current directory for all entries of the Person * type. The search is always only for the current directory, not includding * subdirectories. * * @param $filter The filter should conform to the string representation for * search filters as defined in RFC 2254. If not provided, the default * filter, (objectClass=Person), is used. If provided, it is added as an * logical AND clause to (objectClass=Person). * @param $user The user with reading permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * @param $sort The sorting criteria to apply, in reverse order, as an * array. The keys here are checked against $fields bellow, if that is set, * so don't worry too much. * @param $fields An array containing the LDAP fields you want to * retrieve. The default is to retrive all the available fields for every * entry. * * @return An array where every entry is the CN value of the LDAP entry and * the contents of every entry is an array with the entries fields. An empty * array means the search has not found any matches. A value of FALSE as * return value indicates there were problems. */ function search_person($filter='', $user=FALSE, $sort=array('cn'), $fields=NULL) { if (empty($filter)) $filter='(objectClass=Person)'; else $filter="(&(objectClass=Person)$filter)"; return $this->search_any($filter, $user, $sort, $fields, FALSE); } /** * Returns an array with the entry's properties or FALSE in the case there * are no entries with the assigned CN. * * @param $cn The CN entry of the contact you are searching for. * @param $user The user with reading permissions to do this task. A value * of FALSE will make me attempt to do it anonymously. * @param $fields An array containing the LDAP fields you want to * retrieve. The default is to retrive all the available fields for every * entry. * * @return An array where every entry is the CN value of the LDAP entry and * the contents of every entry is an array with the entries fields. An empty * array means the search has not found any matches. A value of FALSE as * return value indicates there were problems. */ function get_one($cn, $user=FALSE, $fields=NULL) { $result = $this->search_person("(cn=$cn)", $user, array(), $fields); if (count($result) != 1) return FALSE; return $result[0]; } /** * This function will check if a directory exists and either return TRUE * or FALSE * * @param $rdn The reduced distinguished name of subdirectory to find */ function is_dir($dn, $user=FALSE) { $connection = $this->_connect($user); $base = explode(',',$dn); $search_dir = explode('=', $base[0]); unset($base[0]); $base = implode(',',$base); if (!$connection) return FALSE; $result = ldap_list($connection, $base, '(objectClass=organizationalUnit)'); if (!$result) { drupal_set_message(t("I could not check directory in LDAP server"), 'eror'); $this->_disconnect($connection); return FALSE; } $info = $this->_format_entries($connection, $result); foreach ($info as $i) { if ($i[trim($search_dir[0])][0] == trim($search_dir[1])) return TRUE; } return FALSE; } } /** * This class extends the LDAPDirectory class above by fusioning it with the * Drupal preference system. The outcome is that when you create an instance * of this class, all configuration options do not need to be specified. */ class DrupalDirectory extends LDAPDirectory { /** * The constructor, gets the variables from Drupal and builds the underlying * LDAPDirectory. */ function DrupalDirectory ($dir='') { if (empty($dir)) $dir = variable_get('ldap_addressbook_base', 'ou=addressbook,dc=example,dc=com'); parent::LDAPDirectory(variable_get('ldap_addressbook_host', 'localhost'), variable_get('ldap_addressbook_port', '389'), $dir, variable_get('ldap_addressbook_version', '3')); } /** * Returns the user */ function user() { $user = new DirUser(variable_get('ldap_addressbook_user', 'cn=Manager,dc=example,dc=com'), variable_get('ldap_addressbook_user_pass', '')); return $user; } /** * Returns if we should be doing anonymous reading or not */ function read_anonymously() { return (bool)variable_get('ldap_addressbook_anonymous', '1'); } /** * Returns the reader user or FALSE in case I have to read anonymously */ function _reader() { if ($this->read_anonymously()) return FALSE; return $this->user(); } /** * Returns the fields of interest in your addresbook */ function fields() { $default_fields = array('jpegphoto', 'givenname', 'cn', 'sn', 'mail', 'homepostaladdress', 'postaladdress', 'postalcode', 'telephonenumber', 'homephone', 'mobile', 'o', 'labeleduri', 'title'); $retval = strtolower(variable_get('ldap_addressbook_fields', implode(' ', $default_fields))); return explode(' ', $retval); } /** * Returns the main person filter */ function person_filter() { return variable_get('ldap_addressbook_person_filter', '(objectClass=Person)'); } /** * Returns the user addressbook template */ function userbook_template() { return variable_get('ldap_addressbook_private_template', 'ou=%s,ou=addressbook,dc=example,dc=com'); } /** * The password hashing strategy. */ function password_hash() { return variable_get('ldap_addressbook_password_hash', 'clear'); } /** * Returns the subdirectory filter */ function subdir_filter() { return variable_get('ldap_addressbook_subdir_filter', '(objectClass=organizationalUnit)'); } /** * Returns the fields of interest in your addresbook when you want to * compute summary entries (more compact addressbook cards). */ function summary_fields() { $default_summary = array('jpegphoto', 'cn', 'mail', 'telephonenumber', 'mobile', 'labeleduri'); $retval = strtolower(variable_get('ldap_addressbook_summary', implode(' ', $default_summary))); return explode(' ', $retval); } /** * Returns the maximum jpeg size you can have in the addressbook. */ function maximum_jpeg_size() { return (int)variable_get('ldap_addressbook_jpeg_size', 200); } /** * Returns the path that I should use for storing temporary files */ function tmp_path() { return variable_get('ldap_addressbook_tmp', file_directory_path().'/tmp'); } /** * Returns this module name */ function module_name() { return 'ldap_addressbook'; } /** * @see LDAPDirectory::add() */ function add($info, $privacy) { if ($privacy != 'global' && $privacy != 'private') { drupal_set_message(t('Privacy attribute, for addition, can only assume values "global" or "private"'), 'error'); return FALSE; } else if ($privacy == 'global') return parent::add_person($info, $this->user()); else { $user_dn = $this->private_book(); if (!$user_dn) { drupal_set_message(t('Cannot access private addressbook for addition'), 'error'); return FALSE; } $user_dir = new LDAPDirectory (variable_get('ldap_addressbook_host', 'localhost'), variable_get('ldap_addressbook_port', '389'), $user_dn, variable_get('ldap_addressbook_version', '3')); return $user_dir->add_person($info, $this->user()); } } /** * @see LDAPDirectory::delete() */ function delete($cn, $privacy) { if ($privacy != 'global' && $privacy != 'private') { drupal_set_message(t('Privacy attribute, for deletion, can only assume values "global" or "private"'), 'error'); return FALSE; } else if ($privacy == 'global') return parent::delete($cn, $this->user()); else { $user_dn = $this->private_book(); if (!$user_dn) return FALSE; $user_dir = new LDAPDirectory (variable_get('ldap_addressbook_host', 'localhost'), variable_get('ldap_addressbook_port', '389'), $user_dn, variable_get('ldap_addressbook_version', '3')); return $user_dir->delete($cn, $this->user()); } } /** * @see LDAPDirectory::modify() */ function modify ($info) { return parent::modify($info, $this->user()); } /** * @see LDAPDirectory::search() <- problem with recursion * * @param $privacy Defines the scope of the search. A value of "global" will * only search the global addressbook. A value of "private" will only search * the user's addressbook if there is any. A value of "all" will search both * addressbooks. */ function search($filter='', $sort=array('cn'), $fields=NULL, $privacy="all") { $info = array('global' => array(), 'private' => array()); if ($privacy == 'global' || $privacy == 'all') $info['global'] = parent::search_person($filter, $this->_reader(), $sort, $fields); if ($privacy == 'private' || $privacy == 'all') { $user_dn = $this->private_book(); if ($user_dn) { $user_dir = new LDAPDirectory (variable_get('ldap_addressbook_host', 'localhost'), variable_get('ldap_addressbook_port', '389'), $user_dn, variable_get('ldap_addressbook_version', '3')); $info['private'] = $user_dir->search_person($filter, $this->_reader(), $sort, $fields); } else { if ($privacy == 'private') { //if the user is search explicetly for a private addressbook... drupal_set_message(t('You do <b>not</b> seem to have a private addressbook I can search at. Ask the LDAP addressbook administrator to create one for you.'), 'error'); return FALSE; } } } return $info; } /** * Checks if the current user can use/have a private addressbook available. */ function private_book() { if (!user_access('Have private LDAP addressbook')) return FALSE; global $user; //get the current user's name $user_dn = sprintf($this->userbook_template(), $user->name); $check = parent::is_dir($user_dn, $this->_reader()); if ($check) return $user_dn; return FALSE; } /** * @see LDAPDirectory::get_one() */ function get_one($privacy, $cn) { if ($privacy == 'global') return parent::get_one($cn, $this->_reader(), $this->fields()); else if ($privacy == 'private') { if (user_access('Have private LDAP addressbook')) { global $user; //get the current user's name $user_dn = sprintf($this->userbook_template(), $user->name); if (parent::is_dir($user_dn, $this->_reader())) { $user_dir = new LDAPDirectory (variable_get('ldap_addressbook_host', 'localhost'), variable_get('ldap_addressbook_port', '389'), $user_dn, variable_get('ldap_addressbook_version', '3')); return $user_dir->get_one($cn, $this->_reader(), $this->fields()); } } else { drupal_access_denied(); //? return FALSE; } } else { drupal_set_message(t("Cannot accept privacy '$privacy' to get a single entry in the LDAP addressbook. Check the cause of error or submit a bug report!"), 'error'); } drupal_access_denied(); //? } } /** * A hashing system for preparing hashed password to be used with LDAP. This * function source code was copied from the phpLDAPAdmin project. * * @param $secret The value to hash, not previously encripted * @param $algo The algorithm to use. Must be one of crypt, ext_des, md5crypt, * blowfish, md5, sha, smd5, ssha, or clear. * * @return The hashed valued, ready to be used with LDAP, if the algorithm is * supported, or FALSE otherwise. */ function _lab_hash( $secret, $algo ) { $algo = strtolower( $algo ); $hash = ''; switch ( $algo ) { case 'crypt': $hash = '{CRYPT}' . crypt( $secret, random_salt(2) ); break; case 'ext_des': // extended des crypt. see OpenBSD crypt man page. if ( ! defined( 'CRYPT_EXT_DES' ) || CRYPT_EXT_DES == 0 ) { drupal_set_message(t("Your PHP installation does not contain support for the hashing algorithm '$algo'"), 'error'); return FALSE; } $hash = '{CRYPT}' . crypt( $secret, '_' . random_salt(8) ); break; case 'md5crypt': if( ! defined( 'CRYPT_MD5' ) || CRYPT_MD5 == 0 ) { drupal_set_message(t("Your PHP installation does not contain support for the hashing algorithm '$algo'"), 'error'); return FALSE; } $hash = '{CRYPT}' . crypt( $secret , '$1$' . random_salt(9) ); break; case 'blowfish': if( ! defined( 'CRYPT_BLOWFISH' ) || CRYPT_BLOWFISH == 0 ) { drupal_set_message(t("Your PHP installation does not contain support for the hashing algorithm '$algo'"), 'error'); return FALSE; } // hardcoded to second blowfish version and set number of rounds $hash = '{CRYPT}' . crypt( $secret , '$2a$12$' . random_salt(13) ); break; case 'md5': $hash = '{MD5}' . base64_encode( pack( 'H*' , md5($secret) ) ); break; case 'sha': if( function_exists('sha1') ) { // use php 4.3.0+ sha1 function, if it is available. $hash = '{SHA}' . base64_encode( pack( 'H*' , sha1($secret) ) ); } else if ( function_exists( 'mhash' ) ) { $hash = '{SHA}' . base64_encode( mhash( MHASH_SHA1, $secret) ); } else { drupal_set_message(t("Your PHP installation does not contain support for the hashing algorithm '$algo'"), 'error'); return FALSE; } break; case 'ssha': if(function_exists( 'mhash' ) && function_exists('mhash_keygen_s2k')) { mt_srand( (double) microtime() * 1000000 ); $salt = mhash_keygen_s2k(MHASH_SHA1, $secret, substr(pack("h*", md5(mt_rand())), 0, 8), 4); $hash = "{SSHA}".base64_encode(mhash(MHASH_SHA1, $secret.$salt).$salt); } else { drupal_set_message(t("Your PHP installation does not contain support for the hashing algorithm '$algo'"), 'error'); return FALSE; } break; case 'smd5': if(function_exists( 'mhash' ) && function_exists('mhash_keygen_s2k')) { mt_srand( (double) microtime() * 1000000 ); $salt = mhash_keygen_s2k(MHASH_MD5, $secret, substr(pack("h*", md5(mt_rand())), 0, 8), 4); $hash = "{SMD5}".base64_encode(mhash(MHASH_MD5, $secret.$salt).$salt); } else { drupal_set_message(t("Your PHP installation does not contain support for the hashing algorithm '$algo'"), 'error'); return FALSE; } break; case 'clear': default: $hash = $secret; } return $hash; } ?>