⚝
One Hat Cyber Team
⚝
Your IP:
216.73.216.72
Server IP:
41.128.143.86
Server:
Linux host.raqmix.cloud 6.8.0-1025-azure #30~22.04.1-Ubuntu SMP Wed Mar 12 15:28:20 UTC 2025 x86_64
Server Software:
Apache
PHP Version:
8.3.23
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
usr
/
share
/
psa-pear
/
pear
/
php
/
Horde
/
Edit File: Ldap.php
<?php /** * The main Horde_Ldap class. * * Copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger * Copyright 2009-2017 Horde LLC (http://www.horde.org/) * * @package Ldap * @author Tarjej Huse <tarjei@bergfald.no> * @author Jan Wagner <wagner@netsols.de> * @author Del <del@babel.com.au> * @author Benedikt Hallinger <beni@php.net> * @author Ben Klang <ben@alkaloid.net> * @author Chuck Hagenbuch <chuck@horde.org> * @author Jan Schneider <jan@horde.org> * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 */ class Horde_Ldap { /** * Class configuration array. * * - hostspec: The LDAP host to connect to (may be an array of * several hosts to try). * - port: The server port. * - version: LDAP version (defaults to 3). * - tls: When set, ldap_start_tls() is run after connecting. * - binddn: The DN to bind as when searching. * - bindpw: Password to use when searching LDAP. * - basedn: LDAP base. * - options: Hash of LDAP options to set. * - filter: Default search filter. * - scope: Default search scope. * - user: Configuration parameters for {@link findUserDN()}, * must contain 'uid', and may contain 'basedn' * entries. * - timeout: Connection timeout in seconds (defaults to 5). * - auto_reconnect: If true, the class will automatically * attempt to reconnect to the LDAP server in certain * failure conditions when attempting a search, or other * LDAP operations. Defaults to false. Note that if you * set this to true, calls to search() may block * indefinitely if there is a catastrophic server failure. * - min_backoff: Minimum reconnection delay period (in seconds). * - current_backof: Initial reconnection delay period (in seconds). * - max_backoff: Maximum reconnection delay period (in seconds). * - cache: A Horde_Cache instance for caching schema requests. * * @var array */ protected $_config = array( 'hostspec' => 'localhost', 'port' => 389, 'version' => 3, 'tls' => false, 'binddn' => '', 'bindpw' => '', 'basedn' => '', 'options' => array(), 'filter' => '(objectClass=*)', 'scope' => 'sub', 'user' => array(), 'timeout' => 5, 'auto_reconnect' => false, 'min_backoff' => 1, 'current_backoff' => 1, 'max_backoff' => 32, 'cache' => false, 'cache_root_dse' => false, 'cachettl' => 3600); /** * List of hosts we try to establish a connection to. * * @var array */ protected $_hostList = array(); /** * List of hosts that are known to be down. * * @var array */ protected $_downHostList = array(); /** * LDAP resource link. * * @var resource */ protected $_link; /** * Schema object. * * @see schema() * @var Horde_Ldap_Schema */ protected $_schema; /** * Cache for attribute encoding checks. * * @var array Hash with attribute names as key and boolean value * to determine whether they should be utf8 encoded or not. */ protected $_schemaAttrs = array(); /** * Cache for rootDSE objects. * * Hash with requested rootDSE attr names as key and rootDSE * object as value. * * Since the RootDSE object itself may request a rootDSE object, * {@link rootDSE()} caches successful requests. * Internally, Horde_Ldap needs several lookups to this object, so * caching increases performance significally. * * @var array */ protected $_rootDSE = array(); /** * Constructor. * * @see $_config * * @param array $config Configuration array. */ public function __construct($config = array()) { if (!Horde_Util::loadExtension('ldap')) { throw new Horde_Ldap_Exception('No PHP LDAP extension'); } $this->setConfig($config); $this->bind(); } /** * Destructor. */ public function __destruct() { $this->disconnect(); } /** * Sets the internal configuration array. * * @param array $config Configuration hash. */ protected function setConfig($config) { /* Parameter check -- probably should raise an error here if * config is not an array. */ if (!is_array($config)) { return; } foreach ($config as $k => $v) { if (isset($this->_config[$k])) { $this->_config[$k] = $v; } } /* Ensure the host list is an array. */ if (is_array($this->_config['hostspec'])) { $this->_hostList = $this->_config['hostspec']; } else { if (strlen($this->_config['hostspec'])) { $this->_hostList = array($this->_config['hostspec']); } else { $this->_hostList = array(); /* This will cause an error in _connect(), so * the user is notified about the failure. */ } } /* Reset the down host list, which seems like a sensible thing * to do if the config is being reset for some reason. */ $this->_downHostList = array(); } /** * Bind or rebind to the LDAP server. * * This function binds with the given DN and password to the * server. In case no connection has been made yet, it will be * started and STARTTLS issued if appropiate. * * The internal bind configuration is not being updated, so if you * call bind() without parameters, you can rebind with the * credentials provided at first connecting to the server. * * @param string $dn DN for binding. * @param string $password Password for binding. * * @throws Horde_Ldap_Exception */ public function bind($dn = null, $password = null) { /* Fetch current bind credentials. */ if (is_null($dn)) { $dn = $this->_config['binddn']; } if (is_null($password)) { $password = $this->_config['bindpw']; } /* Connect first, if we haven't so far. This will also bind * us to the server. */ if (!$this->_link) { /* Store old credentials so we can revert them later, then * overwrite config with new bind credentials. */ $olddn = $this->_config['binddn']; $oldpw = $this->_config['bindpw']; /* Overwrite bind credentials in config so * _connect() knows about them. */ $this->_config['binddn'] = $dn; $this->_config['bindpw'] = $password; /* Try to connect with provided credentials. */ $msg = $this->_connect(); /* Reset to previous config. */ $this->_config['binddn'] = $olddn; $this->_config['bindpw'] = $oldpw; return; } /* Do the requested bind as we are asked to bind manually. */ if (empty($dn)) { /* Anonymous bind. */ $msg = @ldap_bind($this->_link); } else { /* Privileged bind. */ $msg = @ldap_bind($this->_link, $dn, $password); } if (!$msg) { throw new Horde_Ldap_Exception('Bind failed: ' . @ldap_error($this->_link), @ldap_errno($this->_link)); } } /** * Connects to the LDAP server. * * This function connects to the LDAP server specified in the * configuration, binds and set up the LDAP protocol as needed. * * @throws Horde_Ldap_Exception */ protected function _connect() { /* Connecting is briefly described in RFC1777. Basicly it works like * this: * 1. set up TCP connection * 2. secure that connection if neccessary * 3a. setVersion to tell server which version we want to speak * 3b. perform bind * 3c. setVersion to tell server which version we want to speak * together with a test for supported versions * 4. set additional protocol options */ /* Return if we are already connected. */ if ($this->_link) { return; } /* Connnect to the LDAP server if we are not connected. Note that * ldap_connect() may return a link value even if no connection is * made. We need to do at least one anonymous bind to ensure that a * connection is actually valid. * * See: http://www.php.net/manual/en/function.ldap-connect.php */ /* Default error message in case all connection attempts fail but no * message is set. */ $current_error = new Horde_Ldap_Exception('Unknown connection error'); /* Catch empty $_hostList arrays. */ if (!is_array($this->_hostList) || !count($this->_hostList)) { throw new Horde_Ldap_Exception('No servers configured'); } /* Cycle through the host list. */ foreach ($this->_hostList as $host) { /* Ensure we have a valid string for host name. */ if (is_array($host)) { $current_error = new Horde_Ldap_Exception('No Servers configured'); continue; } /* Skip this host if it is known to be down. */ if (in_array($host, $this->_downHostList)) { continue; } /* Record the host that we are actually connecting to in case we * need it later. */ $this->_config['hostspec'] = $host; /* The ldap extension doesn't allow to provide connection timeouts * and seems to default to 2 minutes. Open a socket manually * instead to ping the server. */ $failed = true; $url = @parse_url($host); $sockhost = !empty($url['host']) ? $url['host'] : $host; if ($fp = @fsockopen($sockhost, $this->_config['port'], $errno, $errstr, $this->_config['timeout'])) { $failed = false; fclose($fp); } /* Attempt a connection. */ if (!$failed) { $this->_link = @ldap_connect($host, $this->_config['port']); } if (!$this->_link) { $current_error = new Horde_Ldap_Exception('Could not connect to ' . $host . ':' . $this->_config['port']); $this->_downHostList[] = $host; continue; } /* If we're supposed to use TLS, do so before we try to bind, as * some strict servers only allow binding via secure * connections. */ if ($this->_config['tls']) { try { $this->startTLS(); } catch (Horde_Ldap_Exception $e) { $current_error = $e; $this->_link = false; $this->_downHostList[] = $host; continue; } } /* Try to set the configured LDAP version on the connection if LDAP * server needs that before binding (eg OpenLDAP). * This could be necessary since RFC 1777 states that the protocol * version has to be set at the bind request. * We use force here which means that the test in the rootDSE is * skipped; this is neccessary, because some strict LDAP servers * only allow to read the LDAP rootDSE (which tells us the * supported protocol versions) with authenticated clients. * This may fail in which case we try again after binding. * In this case, most probably the bind() or setVersion() call * below will also fail, providing error messages. */ $version_set = false; $this->setVersion(0, true); /* Attempt to bind to the server. If we have credentials * configured, we try to use them, otherwise it's an anonymous * bind. * As stated by RFC 1777, the bind request should be the first * operation to be performed after the connection is established. * This may give an protocol error if the server does not support * v2 binds and the above call to setVersion() failed. * If the above call failed, we try an v2 bind here and set the * version afterwards (with checking to the rootDSE). */ try { $this->bind(); } catch (Exception $e) { /* The bind failed, discard link and save error msg. * Then record the host as down and try next one. */ if ($this->errorName($e->getCode()) == 'LDAP_PROTOCOL_ERROR' && !$version_set) { /* Provide a finer grained error message if protocol error * arises because of invalid version. */ $e = new Horde_Ldap_Exception($e->getMessage() . ' (could not set LDAP protocol version to ' . $this->_config['version'].')', $e->getCode()); } $this->_link = false; $current_error = $e; $this->_downHostList[] = $host; continue; } /* Set desired LDAP version if not successfully set before. * Here, a check against the rootDSE is performed, so we get a * error message if the server does not support the version. * The rootDSE entry should tell us which LDAP versions are * supported. However, some strict LDAP servers only allow * bound users to read the rootDSE. */ if (!$version_set) { try { $this->setVersion(); } catch (Exception $e) { $current_error = $e; $this->_link = false; $this->_downHostList[] = $host; continue; } } /* Set LDAP parameters, now that we know we have a valid * connection. */ if (isset($this->_config['options']) && is_array($this->_config['options']) && count($this->_config['options'])) { foreach ($this->_config['options'] as $opt => $val) { try { $this->setOption($opt, $val); } catch (Exception $e) { $current_error = $e; $this->_link = false; $this->_downHostList[] = $host; continue 2; } } } /* At this stage we have connected, bound, and set up options, so * we have a known good LDAP server. Time to go home. */ return; } /* All connection attempts have failed, return the last error. */ throw $current_error; } /** * Reconnects to the LDAP server. * * In case the connection to the LDAP service has dropped out for some * reason, this function will reconnect, and re-bind if a bind has been * attempted in the past. It is probably most useful when the server list * provided to the new() or _connect() function is an array rather than a * single host name, because in that case it will be able to connect to a * failover or secondary server in case the primary server goes down. * * This method just tries to re-establish the current connection. It will * sleep for the current backoff period (seconds) before attempting the * connect, and if the connection fails it will double the backoff period, * but not try again. If you want to ensure a reconnection during a * transient period of server downtime then you need to call this function * in a loop. * * @throws Horde_Ldap_Exception */ protected function _reconnect() { /* Return if we are already connected. */ if ($this->_link) { return; } /* Sleep for a backoff period in seconds. */ sleep($this->_config['current_backoff']); /* Retry all available connections. */ $this->_downHostList = array(); try { $this->_connect(); } catch (Horde_Ldap_Exception $e) { $this->_config['current_backoff'] *= 2; if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { $this->_config['current_backoff'] = $this->_config['max_backoff']; } throw $e; } /* Now we should be able to safely (re-)bind. */ try { $this->bind(); } catch (Exception $e) { $this->_config['current_backoff'] *= 2; if ($this->_config['current_backoff'] > $this->_config['max_backoff']) { $this->_config['current_backoff'] = $this->_config['max_backoff']; } /* $this->_config['hostspec'] should have had the last connected * host stored in it by _connect(). Since we are unable to * bind to that host we can safely assume that it is down or has * some other problem. */ $this->_downHostList[] = $this->_config['hostspec']; throw $e; } /* At this stage we have connected, bound, and set up options, so we * have a known good LDAP server. Time to go home. */ $this->_config['current_backoff'] = $this->_config['min_backoff']; } /** * Closes the LDAP connection. */ public function disconnect() { @ldap_close($this->_link); } /** * Starts an encrypted session. * * @throws Horde_Ldap_Exception */ public function startTLS() { /* First try STARTTLS blindly, some servers don't even allow to receive * the rootDSE without TLS. */ if (@ldap_start_tls($this->_link)) { return; } /* Keep original error. */ $error = 'TLS not started: ' . @ldap_error($this->_link); $errno = @ldap_errno($this->_link); /* Test to see if the server supports TLS at all. * This is done via testing the extensions offered by the server. * The OID 1.3.6.1.4.1.1466.20037 tells whether TLS is supported. */ try { $rootDSE = $this->rootDSE(); } catch (Exception $e) { throw new Horde_Ldap_Exception('Unable to start TLS and unable to fetch rootDSE entry to see if TLS is supported: ' . $e->getMessage(), $e->getCode()); } try { $supported_extensions = $rootDSE->getValue('supportedExtension'); } catch (Exception $e) { throw new Horde_Ldap_Exception('Unable to start TLS and unable to fetch rootDSE attribute "supportedExtension" to see if TLS is supoported: ' . $e->getMessage(), $e->getCode()); } if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) { throw new Horde_Ldap_Exception('Server reports that it does not support TLS'); } throw new Horde_Ldap_Exception($error, $errno); } /** * Adds a new entry to the directory. * * This also links the entry to the connection used for the add, if it was * a fresh entry. * * @see HordeLdap_Entry::createFresh() * * @param Horde_Ldap_Entry $entry An LDAP entry. * * @throws Horde_Ldap_Exception */ public function add(Horde_Ldap_Entry $entry) { /* Continue attempting the add operation in a loop until we get a * success, a definitive failure, or the world ends. */ while (true) { $link = $this->getLink(); if ($link === false) { /* We do not have a successful connection yet. The call to * getLink() would have kept trying if we wanted one. */ throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ' no valid LDAP connection could be found.'); } if (@ldap_add($link, $entry->dn(), $entry->getValues())) { /* Entry successfully added, we should update its Horde_Ldap * reference in case it is not set so far (fresh entry). */ try { $entry->getLDAP(); } catch (Horde_Ldap_Exception $e) { $entry->setLDAP($this); } /* Store that the entry is present inside the directory. */ $entry->markAsNew(false); return; } /* We have a failure. What kind? We may be able to reconnect and * try again. */ $error_code = @ldap_errno($link); if ($this->errorName($error_code) != 'LDAP_OPERATIONS_ERROR' | !$this->_config['auto_reconnect']) { /* Errors other than the above are just passed back to the user * so he may react upon them. */ throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ': ' . ldap_err2str($error_code), $error_code); } /* The server has disconnected before trying the operation. We * should try again, possibly with a different server. */ $this->_link = false; $this->_reconnect(); } } /** * Deletes an entry from the directory. * * @param string|Horde_Ldap_Entry $dn DN string or Horde_Ldap_Entry. * @param boolean $recursive Should we delete all children * recursivelx as well? * @throws Horde_Ldap_Exception */ public function delete($dn, $recursive = false) { if ($dn instanceof Horde_Ldap_Entry) { $dn = $dn->dn(); } if (!is_string($dn)) { throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!'); } /* Recursive delete searches for children and calls delete for them. */ if ($recursive) { $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0); if ($result && @ldap_count_entries($this->_link, $result)) { for ($subentry = @ldap_first_entry($this->_link, $result); $subentry; $subentry = @ldap_next_entry($this->_link, $subentry)) { $this->delete(@ldap_get_dn($this->_link, $subentry), true); } } } /* Continue the delete operation in a loop until we get a success, or a * definitive failure. */ while (true) { $link = $this->getLink(); if (!$link) { /* We do not have a successful connection yet. The call to * getLink() would have kept trying if we wanted one. */ throw new Horde_Ldap_Exception('Could not add entry ' . $dn . ' no valid LDAP connection could be found.'); } $s = @ldap_delete($link, $dn); if ($s) { /* Entry successfully deleted. */ return; } /* We have a failure. What kind? We may be able to reconnect and * try again. */ $error_code = @ldap_errno($link); if ($this->errorName($error_code) == 'LDAP_OPERATIONS_ERROR' && $this->_config['auto_reconnect']) { /* The server has disconnected before trying the operation. We * should try again, possibly with a different server. */ $this->_link = false; $this->_reconnect(); } elseif ($this->errorName($error_code) == 'LDAP_NOT_ALLOWED_ON_NONLEAF') { /* Subentries present, server refused to delete. * Deleting subentries is the clients responsibility, but since * the user may not know of the subentries, we do not force * that here but instead notify the developer so he may take * actions himself. */ throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ' because of subentries. Use the recursive parameter to delete them.', $error_code); } else { /* Errors other than the above catched are just passed back to * the user so he may react upon them. */ throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ': ' . ldap_err2str($error_code), $error_code); } } } /** * Modifies an LDAP entry on the server. * * The $params argument is an array of actions and should be something like * this: * <code> * array('add' => array('attribute1' => array('val1', 'val2'), * 'attribute2' => array('val1')), * 'delete' => array('attribute1'), * 'replace' => array('attribute1' => array('val1')), * 'changes' => array('add' => ..., * 'replace' => ..., * 'delete' => array('attribute1', 'attribute2' => array('val1'))) * </code> * * The order of execution is as following: * 1. adds from 'add' array * 2. deletes from 'delete' array * 3. replaces from 'replace' array * 4. changes (add, replace, delete) in order of appearance * * The function calls the corresponding functions of an Horde_Ldap_Entry * object. A detailed description of array structures can be found there. * * Unlike the modification methods provided by the Horde_Ldap_Entry object, * this method will instantly carry out an update() after each operation, * thus modifying "directly" on the server. * * @see Horde_Ldap_Entry::add() * @see Horde_Ldap_Entry::delete() * @see Horde_Ldap_Entry::replace() * * @param string|Horde_Ldap_Entry $entry DN string or Horde_Ldap_Entry. * @param array $parms Array of changes * * @throws Horde_Ldap_Exception */ public function modify($entry, $parms = array()) { if (is_string($entry)) { $entry = $this->getEntry($entry); } if (!($entry instanceof Horde_Ldap_Entry)) { throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!'); } if ($unknown = array_diff(array_keys($parms), array('add', 'delete', 'replace', 'changes'))) { throw new Horde_Ldap_Exception('Unknown modify action(s): ' . implode(', ', $unknown)); } /* Perform changes mentioned separately. */ foreach (array('add', 'delete', 'replace') as $action) { if (!isset($parms[$action])) { continue; } $entry->$action($parms[$action]); $entry->setLDAP($this); /* Because the ldap_*() functions are called inside * Horde_Ldap_Entry::update(), we have to trap the error codes * issued from that if we want to support reconnection. */ while (true) { try { $entry->update(); break; } catch (Exception $e) { /* We have a failure. What kind? We may be able to * reconnect and try again. */ if ($this->errorName($e->getCode()) != 'LDAP_OPERATIONS_ERROR' || !$this->_config['auto_reconnect']) { /* Errors other than the above catched are just passed * back to the user so he may react upon them. */ throw new Horde_Ldap_Exception('Could not modify entry: ' . $e->getMessage()); } /* The server has disconnected before trying the operation. * We should try again, possibly with a different * server. */ $this->_link = false; $this->_reconnect(); } } } if (!isset($parms['changes']) || !is_array($parms['changes'])) { return; } /* Perform combined changes in 'changes' array. */ foreach ($parms['changes'] as $action => $value) { $this->modify($entry, array($action => $value)); } } /** * Runs an LDAP search query. * * $base and $filter may be ommitted. The one from config will then be * used. $base is either a DN-string or an Horde_Ldap_Entry object in which * case its DN will be used. * * $params may contain: * - scope: The scope which will be used for searching, defaults to 'sub': * - base: Just one entry * - sub: The whole tree * - one: Immediately below $base * - sizelimit: Limit the number of entries returned * (default: 0 = unlimited) * - timelimit: Limit the time spent for searching (default: 0 = unlimited) * - attrsonly: If true, the search will only return the attribute names * - attributes: Array of attribute names, which the entry should contain. * It is good practice to limit this to just the ones you * need. * * You cannot override server side limitations to sizelimit and timelimit: * You can always only lower a given limit. * * @todo implement search controls (sorting etc) * * @param string|Horde_Ldap_Entry $base LDAP searchbase. * @param string|Horde_Ldap_Filter $filter LDAP search filter. * @param array $params Array of options. * * @return Horde_Ldap_Search The search result. * @throws Horde_Ldap_Exception */ public function search($base = null, $filter = null, $params = array()) { if (is_null($base)) { $base = $this->_config['basedn']; } if ($base instanceof Horde_Ldap_Entry) { /* Fetch DN of entry, making searchbase relative to the entry. */ $base = $base->dn(); } if (is_null($filter)) { $filter = $this->_config['filter']; } if ($filter instanceof Horde_Ldap_Filter) { /* Convert Horde_Ldap_Filter to string representation. */ $filter = (string)$filter; } /* Setting search parameters. */ $sizelimit = isset($params['sizelimit']) ? $params['sizelimit'] : 0; $timelimit = isset($params['timelimit']) ? $params['timelimit'] : 0; $attrsonly = isset($params['attrsonly']) ? $params['attrsonly'] : 0; $attributes = isset($params['attributes']) ? $params['attributes'] : array(); /* Ensure $attributes to be an array in case only one attribute name * was given as string. */ if (!is_array($attributes)) { $attributes = array($attributes); } /* Reorganize the $attributes array index keys sometimes there are * problems with not consecutive indexes. */ $attributes = array_values($attributes); /* Scoping makes searches faster! */ $scope = isset($params['scope']) ? $params['scope'] : $this->_config['scope']; switch ($scope) { case 'one': $search_function = 'ldap_list'; break; case 'base': $search_function = 'ldap_read'; break; default: $search_function = 'ldap_search'; } /* Continue attempting the search operation until we get a success or a * definitive failure. */ while (true) { $link = $this->getLink(); $search = @call_user_func($search_function, $link, $base, $filter, $attributes, $attrsonly, $sizelimit, $timelimit); if ($errno = @ldap_errno($link)) { $err = $this->errorName($errno); if ($err == 'LDAP_NO_SUCH_OBJECT' || $err == 'LDAP_SIZELIMIT_EXCEEDED') { return new Horde_Ldap_Search($search, $this, $attributes); } if ($err == 'LDAP_FILTER_ERROR') { /* Bad search filter. */ throw new Horde_Ldap_Exception(ldap_err2str($errno) . ' ($filter)', $errno); } if ($err == 'LDAP_OPERATIONS_ERROR' && $this->_config['auto_reconnect']) { $this->_link = false; $this->_reconnect(); } else { $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope"; throw new Horde_Ldap_Exception(ldap_err2str($errno) . $msg, $errno); } } else { return new Horde_Ldap_Search($search, $this, $attributes); } } } /** * Returns the DN of a user. * * The purpose is to quickly find the full DN of a user so it can be used * to re-bind as this user. This method requires the 'user' configuration * parameter to be set. * * @param string $user The user to find. * * @return string The user's full DN. * @throws Horde_Ldap_Exception * @throws Horde_Exception_NotFound */ public function findUserDN($user) { $filter = Horde_Ldap_Filter::combine( 'and', array(Horde_Ldap_Filter::build($this->_config['user']), Horde_Ldap_Filter::create($this->_config['user']['uid'], 'equals', $user))); $search = $this->search( isset($this->_config['user']['basedn']) ? $this->_config['user']['basedn'] : null, $filter, array('attributes' => array($this->_config['user']['uid']))); if (!$search->count()) { throw new Horde_Exception_NotFound('DN for user ' . $user . ' not found'); } $entry = $search->shiftEntry(); return $entry->currentDN(); } /** * Sets an LDAP option. * * @param string $option Option to set. * @param mixed $value Value to set option to. * * @throws Horde_Ldap_Exception */ public function setOption($option, $value) { if (!$this->_link) { throw new Horde_Ldap_Exception('Could not set LDAP option: No LDAP connection'); } if (!defined($option)) { throw new Horde_Ldap_Exception('Unkown option requested'); } if (@ldap_set_option($this->_link, constant($option), $value)) { return; } $err = @ldap_errno($this->_link); if ($err) { throw new Horde_Ldap_Exception(ldap_err2str($err), $err); } throw new Horde_Ldap_Exception('Unknown error'); } /** * Returns an LDAP option value. * * @param string $option Option to get. * * @return Horde_Ldap_Error|string Horde_Ldap_Error or option value * @throws Horde_Ldap_Exception */ public function getOption($option) { if (!$this->_link) { throw new Horde_Ldap_Exception('No LDAP connection'); } if (!defined($option)) { throw new Horde_Ldap_Exception('Unkown option requested'); } if (@ldap_get_option($this->_link, constant($option), $value)) { return $value; } $err = @ldap_errno($this->_link); if ($err) { throw new Horde_Ldap_Exception(ldap_err2str($err), $err); } throw new Horde_Ldap_Exception('Unknown error'); } /** * Returns the LDAP protocol version that is used on the connection. * * A lot of LDAP functionality is defined by what protocol version * the LDAP server speaks. This might be 2 or 3. * * @return integer The protocol version. */ public function getVersion() { if ($this->_link) { $version = $this->getOption('LDAP_OPT_PROTOCOL_VERSION'); } else { $version = $this->_config['version']; } return $version; } /** * Sets the LDAP protocol version that is used on the connection. * * @todo Checking via the rootDSE takes much time - why? fetching * and instanciation is quick! * * @param integer $version LDAP version that should be used. * @param boolean $force If set to true, the check against the rootDSE * will be skipped. * * @throws Horde_Ldap_Exception */ public function setVersion($version = 0, $force = false) { if (!$version) { $version = $this->_config['version']; } /* Check to see if the server supports this version first. * * TODO: Why is this so horribly slow? $this->rootDSE() is very fast, * as well as Horde_Ldap_RootDse(). Seems like a problem at copying the * object inside PHP?? Additionally, this is not always * reproducable... */ if (!$force) { try { $rootDSE = $this->rootDSE(); $supported_versions = $rootDSE->getValue('supportedLDAPVersion'); if (is_string($supported_versions)) { $supported_versions = array($supported_versions); } $check_ok = in_array($version, $supported_versions); } catch (Horde_Ldap_Exception $e) { /* If we don't get a root DSE, this is probably a v2 server. */ $check_ok = $version < 3; } } $check_ok = true; if ($force || $check_ok) { return $this->setOption('LDAP_OPT_PROTOCOL_VERSION', $version); } throw new Horde_Ldap_Exception('LDAP Server does not support protocol version ' . $version); } /** * Returns whether a DN exists in the directory. * * @param string|Horde_Ldap_Entry $dn The DN of the object to test. * * @return boolean True if the DN exists. * @throws Horde_Ldap_Exception */ public function exists($dn) { if ($dn instanceof Horde_Ldap_Entry) { $dn = $dn->dn(); } if (!is_string($dn)) { throw new Horde_Ldap_Exception('Parameter $dn is not a string nor an entry object!'); } /* Make dn relative to parent. */ $options = array('casefold' => 'none'); $base = Horde_Ldap_Util::explodeDN($dn, $options); $entry_rdn = '(&(' . Horde_Ldap_Util::canonicalDN( array_shift($base), array_merge($options, array('separator' => ')(')) ) . '))'; $base = Horde_Ldap_Util::canonicalDN($base, $options); $result = @ldap_list($this->_link, $base, $entry_rdn, array('dn'), 1, 1); if ($result && @ldap_count_entries($this->_link, $result)) { return true; } if ($this->errorName(@ldap_errno($this->_link)) == 'LDAP_NO_SUCH_OBJECT') { return false; } if (@ldap_errno($this->_link)) { throw new Horde_Ldap_Exception(@ldap_error($this->_link), @ldap_errno($this->_link)); } return false; } /** * Returns a specific entry based on the DN. * * @todo Maybe a check against the schema should be done to be * sure the attribute type exists. * * @param string $dn DN of the entry that should be fetched. * @param array $attributes Array of Attributes to select. If ommitted, all * attributes are fetched. * * @return Horde_Ldap_Entry A Horde_Ldap_Entry object. * @throws Horde_Ldap_Exception * @throws Horde_Exception_NotFound */ public function getEntry($dn, $attributes = array()) { if (!is_array($attributes)) { $attributes = array($attributes); } $result = $this->search($dn, '(objectClass=*)', array('scope' => 'base', 'attributes' => $attributes)); if (!$result->count()) { throw new Horde_Exception_NotFound(sprintf('Could not fetch entry %s: no entry found', $dn)); } $entry = $result->shiftEntry(); if (!$entry) { throw new Horde_Ldap_Exception('Could not fetch entry (error retrieving entry from search result)'); } return $entry; } /** * Renames or moves an entry. * * This method will instantly carry out an update() after the * move, so the entry is moved instantly. * * You can pass an optional Horde_Ldap object. In this case, a * cross directory move will be performed which deletes the entry * in the source (THIS) directory and adds it in the directory * $target_ldap. * * A cross directory move will switch the entry's internal LDAP * reference so updates to the entry will go to the new directory. * * If you want to do a cross directory move, you need to pass an * Horde_Ldap_Entry object, otherwise the attributes will be * empty. * * @param string|Horde_Ldap_Entry $entry An LDAP entry. * @param string $newdn The new location. * @param Horde_Ldap $target_ldap Target directory for cross * server move. * * @throws Horde_Ldap_Exception */ public function move($entry, $newdn, $target_ldap = null) { if (is_string($entry)) { if ($target_ldap && $target_ldap !== $this) { throw new Horde_Ldap_Exception('Unable to perform cross directory move: operation requires a Horde_Ldap_Entry object'); } $entry = $this->getEntry($entry); } if (!$entry instanceof Horde_Ldap_Entry) { throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object! (If DN was passed, conversion failed)'); } if ($target_ldap && !($target_ldap instanceof Horde_Ldap)) { throw new Horde_Ldap_Exception('Parameter $target_ldap is expected to be a Horde_Ldap object!'); } if (!$target_ldap || $target_ldap === $this) { /* Local move. */ $entry->dn($newdn); $entry->setLDAP($this); $entry->update(); return; } /* Cross directory move. */ if ($target_ldap->exists($newdn)) { throw new Horde_Ldap_Exception('Unable to perform cross directory move: entry does exist in target directory'); } $entry->dn($newdn); try { $target_ldap->add($entry); } catch (Exception $e) { throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in target directory'); } try { $this->delete($entry->currentDN()); } catch (Exception $e) { try { $add_error_string = ''; /* Undo add. */ $target_ldap->delete($entry); } catch (Exception $e) { $add_error_string = ' Additionally, the deletion (undo add) of $entry in target directory failed.'; } throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in source directory.' . $add_error_string); } $entry->setLDAP($target_ldap); } /** * Copies an entry to a new location. * * The entry will be immediately copied. Only attributes you have * selected will be copied. * * @param Horde_Ldap_Entry $entry An LDAP entry. * @param string $newdn New FQF-DN of the entry. * * @return Horde_Ldap_Entry The copied entry. * @throws Horde_Ldap_Exception */ public function copy($entry, $newdn) { if (!$entry instanceof Horde_Ldap_Entry) { throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object'); } $newentry = Horde_Ldap_Entry::createFresh($newdn, $entry->getValues()); $this->add($newentry); return $newentry; } /** * Returns the string for an LDAP errorcode. * * Made to be able to make better errorhandling. Function based * on DB::errorMessage(). * * Hint: The best description of the errorcodes is found here: * http://www.directory-info.com/Ldap/LDAPErrorCodes.html * * @param integer $errorcode An error code. * * @return string The description for the error. */ public static function errorName($errorcode) { $errorMessages = array( 0x00 => 'LDAP_SUCCESS', 0x01 => 'LDAP_OPERATIONS_ERROR', 0x02 => 'LDAP_PROTOCOL_ERROR', 0x03 => 'LDAP_TIMELIMIT_EXCEEDED', 0x04 => 'LDAP_SIZELIMIT_EXCEEDED', 0x05 => 'LDAP_COMPARE_FALSE', 0x06 => 'LDAP_COMPARE_TRUE', 0x07 => 'LDAP_AUTH_METHOD_NOT_SUPPORTED', 0x08 => 'LDAP_STRONG_AUTH_REQUIRED', 0x09 => 'LDAP_PARTIAL_RESULTS', 0x0a => 'LDAP_REFERRAL', 0x0b => 'LDAP_ADMINLIMIT_EXCEEDED', 0x0c => 'LDAP_UNAVAILABLE_CRITICAL_EXTENSION', 0x0d => 'LDAP_CONFIDENTIALITY_REQUIRED', 0x0e => 'LDAP_SASL_BIND_INPROGRESS', 0x10 => 'LDAP_NO_SUCH_ATTRIBUTE', 0x11 => 'LDAP_UNDEFINED_TYPE', 0x12 => 'LDAP_INAPPROPRIATE_MATCHING', 0x13 => 'LDAP_CONSTRAINT_VIOLATION', 0x14 => 'LDAP_TYPE_OR_VALUE_EXISTS', 0x15 => 'LDAP_INVALID_SYNTAX', 0x20 => 'LDAP_NO_SUCH_OBJECT', 0x21 => 'LDAP_ALIAS_PROBLEM', 0x22 => 'LDAP_INVALID_DN_SYNTAX', 0x23 => 'LDAP_IS_LEAF', 0x24 => 'LDAP_ALIAS_DEREF_PROBLEM', 0x30 => 'LDAP_INAPPROPRIATE_AUTH', 0x31 => 'LDAP_INVALID_CREDENTIALS', 0x32 => 'LDAP_INSUFFICIENT_ACCESS', 0x33 => 'LDAP_BUSY', 0x34 => 'LDAP_UNAVAILABLE', 0x35 => 'LDAP_UNWILLING_TO_PERFORM', 0x36 => 'LDAP_LOOP_DETECT', 0x3C => 'LDAP_SORT_CONTROL_MISSING', 0x3D => 'LDAP_INDEX_RANGE_ERROR', 0x40 => 'LDAP_NAMING_VIOLATION', 0x41 => 'LDAP_OBJECT_CLASS_VIOLATION', 0x42 => 'LDAP_NOT_ALLOWED_ON_NONLEAF', 0x43 => 'LDAP_NOT_ALLOWED_ON_RDN', 0x44 => 'LDAP_ALREADY_EXISTS', 0x45 => 'LDAP_NO_OBJECT_CLASS_MODS', 0x46 => 'LDAP_RESULTS_TOO_LARGE', 0x47 => 'LDAP_AFFECTS_MULTIPLE_DSAS', 0x50 => 'LDAP_OTHER', 0x51 => 'LDAP_SERVER_DOWN', 0x52 => 'LDAP_LOCAL_ERROR', 0x53 => 'LDAP_ENCODING_ERROR', 0x54 => 'LDAP_DECODING_ERROR', 0x55 => 'LDAP_TIMEOUT', 0x56 => 'LDAP_AUTH_UNKNOWN', 0x57 => 'LDAP_FILTER_ERROR', 0x58 => 'LDAP_USER_CANCELLED', 0x59 => 'LDAP_PARAM_ERROR', 0x5a => 'LDAP_NO_MEMORY', 0x5b => 'LDAP_CONNECT_ERROR', 0x5c => 'LDAP_NOT_SUPPORTED', 0x5d => 'LDAP_CONTROL_NOT_FOUND', 0x5e => 'LDAP_NO_RESULTS_RETURNED', 0x5f => 'LDAP_MORE_RESULTS_TO_RETURN', 0x60 => 'LDAP_CLIENT_LOOP', 0x61 => 'LDAP_REFERRAL_LIMIT_EXCEEDED', 1000 => 'Unknown Error'); return isset($errorMessages[$errorcode]) ? $errorMessages[$errorcode] : 'Unknown Error (' . $errorcode . ')'; } /** * Returns a rootDSE object * * This either fetches a fresh rootDSE object or returns it from * the internal cache for performance reasons, if possible. * * @param array $attrs Array of attributes to search for. * * @return Horde_Ldap_RootDse Horde_Ldap_RootDse object * @throws Horde_Ldap_Exception */ public function rootDSE(array $attrs = array()) { /* If a cache object is registered, we use that to fetch a rootDSE * object. */ $key = 'Horde_Ldap_RootDse_' . md5(serialize(array( $this->_config['hostspec'], $this->_config['port'], $attrs ))); if (empty($this->_rootDSE[$key]) && $this->_config['cache'] && $this->_config['cache_root_dse']) { $entry = $this->_config['cache']->get( $key, $this->_config['cachettl'] ); if ($entry) { $this->_rootDSE[$key] = @unserialize($entry); } } /* See if we need to fetch a fresh object, or if we already * requested this object with the same attributes. */ if (empty($this->_rootDSE[$key])) { $this->_rootDSE[$key] = new Horde_Ldap_RootDse($this, $attrs); /* If caching is active, advise the cache to store the object. */ if ($this->_config['cache'] && $this->_config['cache_root_dse']) { $this->_config['cache']->set( $key, serialize($this->_rootDSE[$key]), $this->_config['cachettl'] ); } } return $this->_rootDSE[$key]; } /** * Returns a schema object * * @param string $dn Subschema entry dn. * * @return Horde_Ldap_Schema Horde_Ldap_Schema object * @throws Horde_Ldap_Exception */ public function schema($dn = null) { /* If a schema caching object is registered, we use that to fetch a * schema object. */ $key = 'Horde_Ldap_Schema_' . md5(serialize(array($this->_config['hostspec'], $this->_config['port'], $dn))); if (!$this->_schema && $this->_config['cache']) { $schema = $this->_config['cache']->get($key, $this->_config['cachettl']); if ($schema) { $this->_schema = @unserialize($schema); } } /* Fetch schema, if not tried before and no cached version available. * If we are already fetching the schema, we will skip fetching. */ if (!$this->_schema) { /* Store a temporary error message so subsequent calls to schema() * can detect that we are fetching the schema already. Otherwise we * will get an infinite loop at Horde_Ldap_Schema. */ $this->_schema = new Horde_Ldap_Exception('Schema not initialized'); $this->_schema = new Horde_Ldap_Schema($this, $dn); /* If schema caching is active, advise the cache to store the * schema. */ if ($this->_config['cache']) { $this->_config['cache']->set($key, serialize($this->_schema), $this->_config['cachettl']); } } if ($this->_schema instanceof Horde_Ldap_Exception) { throw $this->_schema; } return $this->_schema; } /** * Checks if PHP's LDAP extension is loaded. * * If it is not loaded, it tries to load it manually using PHP's dl(). * It knows both windows-dll and *nix-so. * * @throws Horde_Ldap_Exception */ public static function checkLDAPExtension() { if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) { throw new Horde_Ldap_Exception('Unable to locate PHP LDAP extension. Please install it before using the Horde_Ldap package.'); } } /** * @todo Remove this and expect all data to be UTF-8. * * Encodes given attributes to UTF8 if needed by schema. * * This function takes attributes in an array and then checks * against the schema if they need UTF8 encoding. If that is the * case, they will be encoded. An encoded array will be returned * and can be used for adding or modifying. * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code> * * @param array $attributes An array of attributes. * * @return array|Horde_Ldap_Error An array of UTF8 encoded attributes or an error. */ public function utf8Encode($attributes) { return $this->utf8($attributes, 'utf8_encode'); } /** * @todo Remove this and expect all data to be UTF-8. * * Decodes the given attribute values if needed by schema * * $attributes is expected to be an array with keys describing * the attribute names and the values as the value of this attribute: * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code> * * @param array $attributes Array of attributes * * @access public * @see utf8Encode() * @return array|Horde_Ldap_Error Array with decoded attribute values or Error */ public function utf8Decode($attributes) { return $this->utf8($attributes, 'utf8_decode'); } /** * @todo Remove this and expect all data to be UTF-8. * * Encodes or decodes attribute values if needed * * @param array $attributes Array of attributes * @param array $function Function to apply to attribute values * * @access protected * @return array Array of attributes with function applied to values. */ protected function utf8($attributes, $function) { if (!is_array($attributes) || array_key_exists(0, $attributes)) { throw new Horde_Ldap_Exception('Parameter $attributes is expected to be an associative array'); } if (!$this->_schema) { $this->_schema = $this->schema(); } if (!$this->_link || !function_exists($function)) { return $attributes; } if (is_array($attributes) && count($attributes) > 0) { foreach ($attributes as $k => $v) { if (!isset($this->_schemaAttrs[$k])) { try { $attr = $this->_schema->get('attribute', $k); } catch (Exception $e) { continue; } if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) { $encode = true; } else { $encode = false; } $this->_schemaAttrs[$k] = $encode; } else { $encode = $this->_schemaAttrs[$k]; } if ($encode) { if (is_array($v)) { foreach ($v as $ak => $av) { $v[$ak] = call_user_func($function, $av); } } else { $v = call_user_func($function, $v); } } $attributes[$k] = $v; } } return $attributes; } /** * Returns the LDAP link resource. * * It will loop attempting to re-establish the connection if the * connection attempt fails and auto_reconnect has been turned on * (see the _config array documentation). * * @return resource LDAP link. */ public function getLink() { if ($this->_config['auto_reconnect']) { while (true) { /* Return the link handle if we are already connected. * Otherwise try to reconnect. */ if ($this->_link) { return $this->_link; } $this->_reconnect(); } } return $this->_link; } /** * Builds an LDAP search filter fragment. * * @param string $lhs The attribute to test. * @param string $op The operator. * @param string $rhs The comparison value. * @param array $params Any additional parameters for the operator. * * @return string The LDAP search fragment. */ public static function buildClause($lhs, $op, $rhs, $params = array()) { switch ($op) { case 'LIKE': if (empty($rhs)) { return '(' . $lhs . '=*)'; } if (!empty($params['begin'])) { return sprintf('(|(%s=%s*)(%s=* %s*))', $lhs, self::quote($rhs), $lhs, self::quote($rhs)); } if (!empty($params['approximate'])) { return sprintf('(%s~=%s)', $lhs, self::quote($rhs)); } return sprintf('(%s=*%s*)', $lhs, self::quote($rhs)); default: return sprintf('(%s%s%s)', $lhs, $op, self::quote($rhs)); } } /** * Escapes characters with special meaning in LDAP searches. * * @param string $clause The string to escape. * * @return string The escaped string. */ public static function quote($clause) { return str_replace(array('\\', '(', ')', '*', "\0"), array('\\5c', '\(', '\)', '\*', "\\00"), $clause); } /** * Takes an array of DN elements and properly quotes it according to RFC * 1485. * * @param array $parts An array of tuples containing the attribute * name and that attribute's value which make * up the DN. Example: * <code> * $parts = array( * array('cn', 'John Smith'), * array('dc', 'example'), * array('dc', 'com') * ); * </code> * Nested arrays are supported since 2.1.0, to form * multi-valued RDNs. Example: * <code> * $parts = array( * array( * array('cn', 'John'), * array('sn', 'Smith'), * array('o', 'Acme Inc.'), * ), * array('dc', 'example'), * array('dc', 'com') * ); * </code> * which will result in * cn=John+sn=Smith+o=Acme Inc.,dc=example,dc=com * * @return string The properly quoted string DN. */ public static function quoteDN($parts) { return implode(',', array_map('self::_quoteRDNs', $parts)); } /** * Takes a single or a list of RDN arrays with an attribute name and value * and properly quotes it according to RFC 1485. * * @param array $attribute A tuple or array of tuples containing the * attribute name and that attribute's value which * make up the RDN. * * @return string The properly quoted string RDN. */ protected static function _quoteRDNs($attribute) { if (is_array($attribute[0])) { return implode( '+', array_map('self::_quoteRDN', $attribute) ); } else { return self::_quoteRDN($attribute); } } /** * Takes an RDN array with an attribute name and value and properly quotes * it according to RFC 1485. * * @param array $attribute A tuple containing the attribute name and that * attribute's value which make up the RDN. * * @return string The properly quoted string RDN. */ protected static function _quoteRDN($attribute) { $rdn = $attribute[0] . '='; // See if we need to quote the value. if (preg_match('/^\s|\s$|\s\s|[,+="\r\n<>#;]/', $attribute[1])) { $rdn .= '"' . str_replace('"', '\\"', $attribute[1]) . '"'; } else { $rdn .= $attribute[1]; } return $rdn; } }
Simpan