<?php /** * Zend Framework * * LICENSE * * This source file is subject to the new BSD license that is bundled * with this package in the file LICENSE.txt. * It is also available through the world-wide-web at this URL: * http://framework.zend.com/license/new-bsd * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@zend.com so we can send you a copy immediately. * * @category Zend * @package Zend_Ldap * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License * @version $Id$ */ /** * @category Zend * @package Zend_Ldap * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ class Zend_Ldap { const SEARCH_SCOPE_SUB = 1; const SEARCH_SCOPE_ONE = 2; const SEARCH_SCOPE_BASE = 3; const ACCTNAME_FORM_DN = 1; const ACCTNAME_FORM_USERNAME = 2; const ACCTNAME_FORM_BACKSLASH = 3; const ACCTNAME_FORM_PRINCIPAL = 4; /** * String used with ldap_connect for error handling purposes. * * @var string */ private $_connectString; /** * The options used in connecting, binding, etc. * * @var array */ protected $_options = null; /** * The raw LDAP extension resource. * * @var resource */ protected $_resource = null; /** * FALSE if no user is bound to the LDAP resource * NULL if there has been an anonymous bind * username of the currently bound user * * @var boolean|null|string */ protected $_boundUser = false; /** * Caches the RootDSE * * @var Zend_Ldap_Node */ protected $_rootDse = null; /** * Caches the schema * * @var Zend_Ldap_Node */ protected $_schema = null; /** * @deprecated will be removed, use {@see Zend_Ldap_Filter_Abstract::escapeValue()} * @param string $str The string to escape. * @return string The escaped string */ public static function filterEscape($str) { /** * @see Zend_Ldap_Filter_Abstract */ require_once 'Zend/Ldap/Filter/Abstract.php'; return Zend_Ldap_Filter_Abstract::escapeValue($str); } /** * @deprecated will be removed, use {@see Zend_Ldap_Dn::checkDn()} * @param string $dn The DN to parse * @param array $keys An optional array to receive DN keys (e.g. CN, OU, DC, ...) * @param array $vals An optional array to receive DN values * @return boolean True if the DN was successfully parsed or false if the string is * not a valid DN. */ public static function explodeDn($dn, array &$keys = null, array &$vals = null) { /** * @see Zend_Ldap_Dn */ require_once 'Zend/Ldap/Dn.php'; return Zend_Ldap_Dn::checkDn($dn, $keys, $vals); } /** * Constructor. * * @param array|Zend_Config $options Options used in connecting, binding, etc. * @return void * @throws Zend_Ldap_Exception if ext/ldap is not installed */ public function __construct($options = array()) { if (!extension_loaded('ldap')) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'LDAP extension not loaded', Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED); } $this->setOptions($options); } /** * Destructor. * * @return void */ public function __destruct() { $this->disconnect(); } /** * @return resource The raw LDAP extension resource. */ public function getResource() { if (!is_resource($this->_resource) || $this->_boundUser === false) { $this->bind(); } return $this->_resource; } /** * Return the LDAP error number of the last LDAP command * * @return int */ public function getLastErrorCode() { $ret = @ldap_get_option($this->_resource, LDAP_OPT_ERROR_NUMBER, $err); if ($ret === true) { if ($err <= -1 && $err >= -17) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; /* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error * codes in OpenLDAP are negative values from -1 to -17. */ $err = Zend_Ldap_Exception::LDAP_SERVER_DOWN + (-$err - 1); } return $err; } return 0; } /** * Return the LDAP error message of the last LDAP command * * @param int $errorCode * @param array $errorMessages * @return string */ public function getLastError(&$errorCode = null, array &$errorMessages = null) { $errorCode = $this->getLastErrorCode(); $errorMessages = array(); /* The various error retrieval functions can return * different things so we just try to collect what we * can and eliminate dupes. */ $estr1 = @ldap_error($this->_resource); if ($errorCode !== 0 && $estr1 === 'Success') { $estr1 = @ldap_err2str($errorCode); } if (!empty($estr1)) { $errorMessages[] = $estr1; } @ldap_get_option($this->_resource, LDAP_OPT_ERROR_STRING, $estr2); if (!empty($estr2) && !in_array($estr2, $errorMessages)) { $errorMessages[] = $estr2; } $message = ''; if ($errorCode > 0) { $message = '0x' . dechex($errorCode) . ' '; } else { $message = ''; } if (count($errorMessages) > 0) { $message .= '(' . implode('; ', $errorMessages) . ')'; } else { $message .= '(no error message from LDAP)'; } return $message; } /** * Get the currently bound user * * FALSE if no user is bound to the LDAP resource * NULL if there has been an anonymous bind * username of the currently bound user * * @return false|null|string */ public function getBoundUser() { return $this->_boundUser; } /** * Sets the options used in connecting, binding, etc. * * Valid option keys: * host * port * useSsl * username * password * bindRequiresDn * baseDn * accountCanonicalForm * accountDomainName * accountDomainNameShort * accountFilterFormat * allowEmptyPassword * useStartTls * optRefferals * tryUsernameSplit * * @param array|Zend_Config $options Options used in connecting, binding, etc. * @return Zend_Ldap Provides a fluent interface * @throws Zend_Ldap_Exception */ public function setOptions($options) { if ($options instanceof Zend_Config) { $options = $options->toArray(); } $permittedOptions = array( 'host' => null, 'port' => 0, 'useSsl' => false, 'username' => null, 'password' => null, 'bindRequiresDn' => false, 'baseDn' => null, 'accountCanonicalForm' => null, 'accountDomainName' => null, 'accountDomainNameShort' => null, 'accountFilterFormat' => null, 'allowEmptyPassword' => false, 'useStartTls' => false, 'optReferrals' => false, 'tryUsernameSplit' => true, ); foreach ($permittedOptions as $key => $val) { if (array_key_exists($key, $options)) { $val = $options[$key]; unset($options[$key]); /* Enforce typing. This eliminates issues like Zend_Config_Ini * returning '1' as a string (ZF-3163). */ switch ($key) { case 'port': case 'accountCanonicalForm': $permittedOptions[$key] = (int)$val; break; case 'useSsl': case 'bindRequiresDn': case 'allowEmptyPassword': case 'useStartTls': case 'optReferrals': case 'tryUsernameSplit': $permittedOptions[$key] = ($val === true || $val === '1' || strcasecmp($val, 'true') == 0); break; default: $permittedOptions[$key] = trim($val); break; } } } if (count($options) > 0) { $key = key($options); /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Unknown Zend_Ldap option: $key"); } $this->_options = $permittedOptions; return $this; } /** * @return array The current options. */ public function getOptions() { return $this->_options; } /** * @return string The hostname of the LDAP server being used to authenticate accounts */ protected function _getHost() { return $this->_options['host']; } /** * @return int The port of the LDAP server or 0 to indicate that no port value is set */ protected function _getPort() { return $this->_options['port']; } /** * @return boolean The default SSL / TLS encrypted transport control */ protected function _getUseSsl() { return $this->_options['useSsl']; } /** * @return string The default acctname for binding */ protected function _getUsername() { return $this->_options['username']; } /** * @return string The default password for binding */ protected function _getPassword() { return $this->_options['password']; } /** * @return boolean Bind requires DN */ protected function _getBindRequiresDn() { return $this->_options['bindRequiresDn']; } /** * Gets the base DN under which objects of interest are located * * @return string */ public function getBaseDn() { return $this->_options['baseDn']; } /** * @return integer Either ACCTNAME_FORM_BACKSLASH, ACCTNAME_FORM_PRINCIPAL or * ACCTNAME_FORM_USERNAME indicating the form usernames should be canonicalized to. */ protected function _getAccountCanonicalForm() { /* Account names should always be qualified with a domain. In some scenarios * using non-qualified account names can lead to security vulnerabilities. If * no account canonical form is specified, we guess based in what domain * names have been supplied. */ $accountCanonicalForm = $this->_options['accountCanonicalForm']; if (!$accountCanonicalForm) { $accountDomainName = $this->_getAccountDomainName(); $accountDomainNameShort = $this->_getAccountDomainNameShort(); if ($accountDomainNameShort) { $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_BACKSLASH; } else if ($accountDomainName) { $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_PRINCIPAL; } else { $accountCanonicalForm = Zend_Ldap::ACCTNAME_FORM_USERNAME; } } return $accountCanonicalForm; } /** * @return string The account domain name */ protected function _getAccountDomainName() { return $this->_options['accountDomainName']; } /** * @return string The short account domain name */ protected function _getAccountDomainNameShort() { return $this->_options['accountDomainNameShort']; } /** * @return string A format string for building an LDAP search filter to match * an account */ protected function _getAccountFilterFormat() { return $this->_options['accountFilterFormat']; } /** * @return boolean Allow empty passwords */ protected function _getAllowEmptyPassword() { return $this->_options['allowEmptyPassword']; } /** * @return boolean The default SSL / TLS encrypted transport control */ protected function _getUseStartTls() { return $this->_options['useStartTls']; } /** * @return boolean Opt. Referrals */ protected function _getOptReferrals() { return $this->_options['optReferrals']; } /** * @return boolean Try splitting the username into username and domain */ protected function _getTryUsernameSplit() { return $this->_options['tryUsernameSplit']; } /** * @return string The LDAP search filter for matching directory accounts */ protected function _getAccountFilter($acctname) { /** * @see Zend_Ldap_Filter_Abstract */ require_once 'Zend/Ldap/Filter/Abstract.php'; $this->_splitName($acctname, $dname, $aname); $accountFilterFormat = $this->_getAccountFilterFormat(); $aname = Zend_Ldap_Filter_Abstract::escapeValue($aname); if ($accountFilterFormat) { return sprintf($accountFilterFormat, $aname); } if (!$this->_getBindRequiresDn()) { // is there a better way to detect this? return sprintf("(&(objectClass=user)(sAMAccountName=%s))", $aname); } return sprintf("(&(objectClass=posixAccount)(uid=%s))", $aname); } /** * @param string $name The name to split * @param string $dname The resulting domain name (this is an out parameter) * @param string $aname The resulting account name (this is an out parameter) * @return void */ protected function _splitName($name, &$dname, &$aname) { $dname = null; $aname = $name; if (!$this->_getTryUsernameSplit()) { return; } $pos = strpos($name, '@'); if ($pos) { $dname = substr($name, $pos + 1); $aname = substr($name, 0, $pos); } else { $pos = strpos($name, '\\'); if ($pos) { $dname = substr($name, 0, $pos); $aname = substr($name, $pos + 1); } } } /** * @param string $acctname The name of the account * @return string The DN of the specified account * @throws Zend_Ldap_Exception */ protected function _getAccountDn($acctname) { /** * @see Zend_Ldap_Dn */ require_once 'Zend/Ldap/Dn.php'; if (Zend_Ldap_Dn::checkDn($acctname)) return $acctname; $acctname = $this->getCanonicalAccountName($acctname, Zend_Ldap::ACCTNAME_FORM_USERNAME); $acct = $this->_getAccount($acctname, array('dn')); return $acct['dn']; } /** * @param string $dname The domain name to check * @return boolean */ protected function _isPossibleAuthority($dname) { if ($dname === null) { return true; } $accountDomainName = $this->_getAccountDomainName(); $accountDomainNameShort = $this->_getAccountDomainNameShort(); if ($accountDomainName === null && $accountDomainNameShort === null) { return true; } if (strcasecmp($dname, $accountDomainName) == 0) { return true; } if (strcasecmp($dname, $accountDomainNameShort) == 0) { return true; } return false; } /** * @param string $acctname The name to canonicalize * @param int $type The desired form of canonicalization * @return string The canonicalized name in the desired form * @throws Zend_Ldap_Exception */ public function getCanonicalAccountName($acctname, $form = 0) { $this->_splitName($acctname, $dname, $uname); if (!$this->_isPossibleAuthority($dname)) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Binding domain is not an authority for user: $acctname", Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH); } if (!$uname) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Invalid account name syntax: $acctname"); } if (function_exists('mb_strtolower')) { $uname = mb_strtolower($uname, 'UTF-8'); } else { $uname = strtolower($uname); } if ($form === 0) { $form = $this->_getAccountCanonicalForm(); } switch ($form) { case Zend_Ldap::ACCTNAME_FORM_DN: return $this->_getAccountDn($acctname); case Zend_Ldap::ACCTNAME_FORM_USERNAME: return $uname; case Zend_Ldap::ACCTNAME_FORM_BACKSLASH: $accountDomainNameShort = $this->_getAccountDomainNameShort(); if (!$accountDomainNameShort) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Option required: accountDomainNameShort'); } return "$accountDomainNameShort\\$uname"; case Zend_Ldap::ACCTNAME_FORM_PRINCIPAL: $accountDomainName = $this->_getAccountDomainName(); if (!$accountDomainName) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Option required: accountDomainName'); } return "$uname@$accountDomainName"; default: /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Unknown canonical name form: $form"); } } /** * @param array $attrs An array of names of desired attributes * @return array An array of the attributes representing the account * @throws Zend_Ldap_Exception */ protected function _getAccount($acctname, array $attrs = null) { $baseDn = $this->getBaseDn(); if (!$baseDn) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Base DN not set'); } $accountFilter = $this->_getAccountFilter($acctname); if (!$accountFilter) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Invalid account filter'); } if (!is_resource($this->getResource())) { $this->bind(); } $accounts = $this->search($accountFilter, $baseDn, self::SEARCH_SCOPE_SUB, $attrs); $count = $accounts->count(); if ($count === 1) { $acct = $accounts->getFirst(); $accounts->close(); return $acct; } else if ($count === 0) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $code = Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT; $str = "No object found for: $accountFilter"; } else { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $code = Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR; $str = "Unexpected result count ($count) for: $accountFilter"; } $accounts->close(); /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, $str, $code); } /** * @return Zend_Ldap Provides a fluent interface */ public function disconnect() { if (is_resource($this->_resource)) { @ldap_unbind($this->_resource); } $this->_resource = null; $this->_boundUser = false; return $this; } /** * To connect using SSL it seems the client tries to verify the server * certificate by default. One way to disable this behavior is to set * 'TLS_REQCERT never' in OpenLDAP's ldap.conf and restarting Apache. Or, * if you really care about the server's cert you can put a cert on the * web server. * * @param string $host The hostname of the LDAP server to connect to * @param int $port The port number of the LDAP server to connect to * @param boolean $useSsl Use SSL * @param boolean $useStartTls Use STARTTLS * @return Zend_Ldap Provides a fluent interface * @throws Zend_Ldap_Exception */ public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null) { if ($host === null) { $host = $this->_getHost(); } if ($port === null) { $port = $this->_getPort(); } else { $port = (int)$port; } if ($useSsl === null) { $useSsl = $this->_getUseSsl(); } else { $useSsl = (bool)$useSsl; } if ($useStartTls === null) { $useStartTls = $this->_getUseStartTls(); } else { $useStartTls = (bool)$useStartTls; } if (!$host) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'A host parameter is required'); } $useUri = false; /* Because ldap_connect doesn't really try to connect, any connect error * will actually occur during the ldap_bind call. Therefore, we save the * connect string here for reporting it in error handling in bind(). */ $hosts = array(); if (preg_match_all('~ldap(?:i|s)?://~', $host, $hosts, PREG_SET_ORDER) > 0) { $this->_connectString = $host; $useUri = true; $useSsl = false; } else { if ($useSsl) { $this->_connectString = 'ldaps://' . $host; $useUri = true; } else { $this->_connectString = 'ldap://' . $host; } if ($port) { $this->_connectString .= ':' . $port; } } $this->disconnect(); /* Only OpenLDAP 2.2 + supports URLs so if SSL is not requested, just * use the old form. */ $resource = ($useUri) ? @ldap_connect($this->_connectString) : @ldap_connect($host, $port); if (is_resource($resource) === true) { $this->_resource = $resource; $this->_boundUser = false; $optReferrals = ($this->_getOptReferrals()) ? 1 : 0; if (@ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3) && @ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals)) { if ($useSsl || !$useStartTls || @ldap_start_tls($resource)) { return $this; } } /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $zle = new Zend_Ldap_Exception($this, "$host:$port"); $this->disconnect(); throw $zle; } /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Failed to connect to LDAP server: $host:$port"); } /** * @param string $username The username for authenticating the bind * @param string $password The password for authenticating the bind * @return Zend_Ldap Provides a fluent interface * @throws Zend_Ldap_Exception */ public function bind($username = null, $password = null) { $moreCreds = true; if ($username === null) { $username = $this->_getUsername(); $password = $this->_getPassword(); $moreCreds = false; } if (empty($username)) { /* Perform anonymous bind */ $username = null; $password = null; } else { /* Check to make sure the username is in DN form. */ /** * @see Zend_Ldap_Dn */ require_once 'Zend/Ldap/Dn.php'; if (!Zend_Ldap_Dn::checkDn($username)) { if ($this->_getBindRequiresDn()) { /* moreCreds stops an infinite loop if _getUsername does not * return a DN and the bind requires it */ if ($moreCreds) { try { $username = $this->_getAccountDn($username); } catch (Zend_Ldap_Exception $zle) { switch ($zle->getCode()) { case Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT: case Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH: case Zend_Ldap_Exception::LDAP_X_EXTENSION_NOT_LOADED: throw $zle; } throw new Zend_Ldap_Exception(null, 'Failed to retrieve DN for account: ' . $username . ' [' . $zle->getMessage() . ']', Zend_Ldap_Exception::LDAP_OPERATIONS_ERROR); } } else { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, 'Binding requires username in DN form'); } } else { $username = $this->getCanonicalAccountName($username, $this->_getAccountCanonicalForm()); } } } if (!is_resource($this->_resource)) { $this->connect(); } if ($username !== null && $password === '' && $this->_getAllowEmptyPassword() !== true) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; $zle = new Zend_Ldap_Exception(null, 'Empty password not allowed - see allowEmptyPassword option.'); } else { if (@ldap_bind($this->_resource, $username, $password)) { $this->_boundUser = $username; return $this; } $message = ($username === null) ? $this->_connectString : $username; /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; switch ($this->getLastErrorCode()) { case Zend_Ldap_Exception::LDAP_SERVER_DOWN: /* If the error is related to establishing a connection rather than binding, * the connect string is more informative than the username. */ $message = $this->_connectString; } $zle = new Zend_Ldap_Exception($this, $message); } $this->disconnect(); throw $zle; } /** * A global LDAP search routine for finding information. * * Options can be either passed as single parameters according to the * method signature or as an array with one or more of the following keys * - filter * - baseDn * - scope * - attributes * - sort * - collectionClass * - sizelimit * - timelimit * * @param string|Zend_Ldap_Filter_Abstract|array $filter * @param string|Zend_Ldap_Dn|null $basedn * @param integer $scope * @param array $attributes * @param string|null $sort * @param string|null $collectionClass * @param integer $sizelimit * @param integer $timelimit * @return Zend_Ldap_Collection * @throws Zend_Ldap_Exception */ public function search($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, array $attributes = array(), $sort = null, $collectionClass = null, $sizelimit = 0, $timelimit = 0) { if (is_array($filter)) { $options = array_change_key_case($filter, CASE_LOWER); foreach ($options as $key => $value) { switch ($key) { case 'filter': case 'basedn': case 'scope': case 'sort': $$key = $value; break; case 'attributes': if (is_array($value)) { $attributes = $value; } break; case 'collectionclass': $collectionClass = $value; break; case 'sizelimit': case 'timelimit': $$key = (int)$value; } } } if ($basedn === null) { $basedn = $this->getBaseDn(); } else if ($basedn instanceof Zend_Ldap_Dn) { $basedn = $basedn->toString(); } if ($filter instanceof Zend_Ldap_Filter_Abstract) { $filter = $filter->toString(); } switch ($scope) { case self::SEARCH_SCOPE_ONE: $search = @ldap_list($this->getResource(), $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); break; case self::SEARCH_SCOPE_BASE: $search = @ldap_read($this->getResource(), $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); break; case self::SEARCH_SCOPE_SUB: default: $search = @ldap_search($this->getResource(), $basedn, $filter, $attributes, 0, $sizelimit, $timelimit); break; } if($search === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'searching: ' . $filter); } if ($sort !== null && is_string($sort)) { $isSorted = @ldap_sort($this->getResource(), $search, $sort); if($isSorted === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'sorting: ' . $sort); } } /** * Zend_Ldap_Collection_Iterator_Default */ require_once 'Zend/Ldap/Collection/Iterator/Default.php'; $iterator = new Zend_Ldap_Collection_Iterator_Default($this, $search); return $this->_createCollection($iterator, $collectionClass); } /** * Extension point for collection creation * * @param Zend_Ldap_Collection_Iterator_Default $iterator * @param string|null $collectionClass * @return Zend_Ldap_Collection * @throws Zend_Ldap_Exception */ protected function _createCollection(Zend_Ldap_Collection_Iterator_Default $iterator, $collectionClass) { if ($collectionClass === null) { /** * Zend_Ldap_Collection */ require_once 'Zend/Ldap/Collection.php'; return new Zend_Ldap_Collection($iterator); } else { $collectionClass = (string)$collectionClass; if (!class_exists($collectionClass)) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Class '$collectionClass' can not be found"); } if (!is_subclass_of($collectionClass, 'Zend_Ldap_Collection')) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception(null, "Class '$collectionClass' must subclass 'Zend_Ldap_Collection'"); } return new $collectionClass($iterator); } } /** * Count items found by given filter. * * @param string|Zend_Ldap_Filter_Abstract $filter * @param string|Zend_Ldap_Dn|null $basedn * @param integer $scope * @return integer * @throws Zend_Ldap_Exception */ public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB) { try { $result = $this->search($filter, $basedn, $scope, array('dn'), null); } catch (Zend_Ldap_Exception $e) { if ($e->getCode() === Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) return 0; else throw $e; } return $result->count(); } /** * Count children for a given DN. * * @param string|Zend_Ldap_Dn $dn * @return integer * @throws Zend_Ldap_Exception */ public function countChildren($dn) { return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE); } /** * Check if a given DN exists. * * @param string|Zend_Ldap_Dn $dn * @return boolean * @throws Zend_Ldap_Exception */ public function exists($dn) { return ($this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_BASE) == 1); } /** * Search LDAP registry for entries matching filter and optional attributes * * Options can be either passed as single parameters according to the * method signature or as an array with one or more of the following keys * - filter * - baseDn * - scope * - attributes * - sort * - reverseSort * - sizelimit * - timelimit * * @param string|Zend_Ldap_Filter_Abstract|array $filter * @param string|Zend_Ldap_Dn|null $basedn * @param integer $scope * @param array $attributes * @param string|null $sort * @param boolean $reverseSort * @param integer $sizelimit * @param integer $timelimit * @return array * @throws Zend_Ldap_Exception */ public function searchEntries($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB, array $attributes = array(), $sort = null, $reverseSort = false, $sizelimit = 0, $timelimit = 0) { if (is_array($filter)) { $filter = array_change_key_case($filter, CASE_LOWER); if (isset($filter['collectionclass'])) { unset($filter['collectionclass']); } if (isset($filter['reversesort'])) { $reverseSort = $filter['reversesort']; unset($filter['reversesort']); } } $result = $this->search($filter, $basedn, $scope, $attributes, $sort, null, $sizelimit, $timelimit); $items = $result->toArray(); if ((bool)$reverseSort === true) { $items = array_reverse($items, false); } return $items; } /** * Get LDAP entry by DN * * @param string|Zend_Ldap_Dn $dn * @param array $attributes * @param boolean $throwOnNotFound * @return array * @throws Zend_Ldap_Exception */ public function getEntry($dn, array $attributes = array(), $throwOnNotFound = false) { try { $result = $this->search("(objectClass=*)", $dn, self::SEARCH_SCOPE_BASE, $attributes, null); return $result->getFirst(); } catch (Zend_Ldap_Exception $e){ if ($throwOnNotFound !== false) throw $e; } return null; } /** * Prepares an ldap data entry array for insert/update operation * * @param array $entry * @return void * @throws InvalidArgumentException */ public static function prepareLdapEntryArray(array &$entry) { if (array_key_exists('dn', $entry)) unset($entry['dn']); foreach ($entry as $key => $value) { if (is_array($value)) { foreach ($value as $i => $v) { if ($v === null) unset($value[$i]); else if (!is_scalar($v)) { throw new InvalidArgumentException('Only scalar values allowed in LDAP data'); } else { $v = (string)$v; if (strlen($v) == 0) { unset($value[$i]); } else { $value[$i] = $v; } } } $entry[$key] = array_values($value); } else { if ($value === null) $entry[$key] = array(); else if (!is_scalar($value)) { throw new InvalidArgumentException('Only scalar values allowed in LDAP data'); } else { $value = (string)$value; if (strlen($value) == 0) { $entry[$key] = array(); } else { $entry[$key] = array($value); } } } } $entry = array_change_key_case($entry, CASE_LOWER); } /** * Add new information to the LDAP repository * * @param string|Zend_Ldap_Dn $dn * @param array $entry * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function add($dn, array $entry) { if (!($dn instanceof Zend_Ldap_Dn)) { $dn = Zend_Ldap_Dn::factory($dn, null); } self::prepareLdapEntryArray($entry); foreach ($entry as $key => $value) { if (is_array($value) && count($value) === 0) { unset($entry[$key]); } } $rdnParts = $dn->getRdn(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER); foreach ($rdnParts as $key => $value) { $value = Zend_Ldap_Dn::unescapeValue($value); if (!array_key_exists($key, $entry)) { $entry[$key] = array($value); } else if (!in_array($value, $entry[$key])) { $entry[$key] = array_merge(array($value), $entry[$key]); } } $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); foreach ($adAttributes as $attr) { if (array_key_exists($attr, $entry)) { unset($entry[$attr]); } } $isAdded = @ldap_add($this->getResource(), $dn->toString(), $entry); if($isAdded === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'adding: ' . $dn->toString()); } return $this; } /** * Update LDAP registry * * @param string|Zend_Ldap_Dn $dn * @param array $entry * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function update($dn, array $entry) { if (!($dn instanceof Zend_Ldap_Dn)) { $dn = Zend_Ldap_Dn::factory($dn, null); } self::prepareLdapEntryArray($entry); $rdnParts = $dn->getRdn(Zend_Ldap_Dn::ATTR_CASEFOLD_LOWER); foreach ($rdnParts as $key => $value) { $value = Zend_Ldap_Dn::unescapeValue($value); if (array_key_exists($key, $entry) && !in_array($value, $entry[$key])) { $entry[$key] = array_merge(array($value), $entry[$key]); } } $adAttributes = array('distinguishedname', 'instancetype', 'name', 'objectcategory', 'objectguid', 'usnchanged', 'usncreated', 'whenchanged', 'whencreated'); foreach ($adAttributes as $attr) { if (array_key_exists($attr, $entry)) { unset($entry[$attr]); } } if (count($entry) > 0) { $isModified = @ldap_modify($this->getResource(), $dn->toString(), $entry); if($isModified === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'updating: ' . $dn->toString()); } } return $this; } /** * Save entry to LDAP registry. * * Internally decides if entry will be updated to added by calling * {@link exists()}. * * @param string|Zend_Ldap_Dn $dn * @param array $entry * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function save($dn, array $entry) { if ($dn instanceof Zend_Ldap_Dn) { $dn = $dn->toString(); } if ($this->exists($dn)) $this->update($dn, $entry); else $this->add($dn, $entry); return $this; } /** * Delete an LDAP entry * * @param string|Zend_Ldap_Dn $dn * @param boolean $recursively * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function delete($dn, $recursively = false) { if ($dn instanceof Zend_Ldap_Dn) { $dn = $dn->toString(); } if ($recursively === true) { if ($this->countChildren($dn)>0) { $children = $this->_getChildrenDns($dn); foreach ($children as $c) { $this->delete($c, true); } } } $isDeleted = @ldap_delete($this->getResource(), $dn); if($isDeleted === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'deleting: ' . $dn); } return $this; } /** * Retrieve the immediate children DNs of the given $parentDn * * This method is used in recursive methods like {@see delete()} * or {@see copy()} * * @param string|Zend_Ldap_Dn $parentDn * @return array of DNs */ protected function _getChildrenDns($parentDn) { if ($parentDn instanceof Zend_Ldap_Dn) { $parentDn = $parentDn->toString(); } $children = array(); $search = @ldap_list($this->getResource(), $parentDn, '(objectClass=*)', array('dn')); for ($entry = @ldap_first_entry($this->getResource(), $search); $entry !== false; $entry = @ldap_next_entry($this->getResource(), $entry)) { $childDn = @ldap_get_dn($this->getResource(), $entry); if ($childDn === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'getting dn'); } $children[] = $childDn; } @ldap_free_result($search); return $children; } /** * Moves a LDAP entry from one DN to another subtree. * * @param string|Zend_Ldap_Dn $from * @param string|Zend_Ldap_Dn $to * @param boolean $recursively * @param boolean $alwaysEmulate * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false) { if ($from instanceof Zend_Ldap_Dn) { $orgDnParts = $from->toArray(); } else { $orgDnParts = Zend_Ldap_Dn::explodeDn($from); } if ($to instanceof Zend_Ldap_Dn) { $newParentDnParts = $to->toArray(); } else { $newParentDnParts = Zend_Ldap_Dn::explodeDn($to); } $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts); $newDn = Zend_Ldap_Dn::fromArray($newDnParts); return $this->rename($from, $newDn, $recursively, $alwaysEmulate); } /** * Moves a LDAP entry from one DN to another DN. * * This is an alias for {@link rename()} * * @param string|Zend_Ldap_Dn $from * @param string|Zend_Ldap_Dn $to * @param boolean $recursively * @param boolean $alwaysEmulate * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function move($from, $to, $recursively = false, $alwaysEmulate = false) { return $this->rename($from, $to, $recursively, $alwaysEmulate); } /** * Renames a LDAP entry from one DN to another DN. * * This method implicitely moves the entry to another location within the tree. * * @param string|Zend_Ldap_Dn $from * @param string|Zend_Ldap_Dn $to * @param boolean $recursively * @param boolean $alwaysEmulate * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function rename($from, $to, $recursively = false, $alwaysEmulate = false) { $emulate = (bool)$alwaysEmulate; if (!function_exists('ldap_rename')) $emulate = true; else if ($recursively) $emulate = true; if ($emulate === false) { if ($from instanceof Zend_Ldap_Dn) { $from = $from->toString(); } if ($to instanceof Zend_Ldap_Dn) { $newDnParts = $to->toArray(); } else { $newDnParts = Zend_Ldap_Dn::explodeDn($to); } $newRdn = Zend_Ldap_Dn::implodeRdn(array_shift($newDnParts)); $newParent = Zend_Ldap_Dn::implodeDn($newDnParts); $isOK = @ldap_rename($this->getResource(), $from, $newRdn, $newParent, true); if($isOK === false) { /** * @see Zend_Ldap_Exception */ require_once 'Zend/Ldap/Exception.php'; throw new Zend_Ldap_Exception($this, 'renaming ' . $from . ' to ' . $to); } else if (!$this->exists($to)) $emulate = true; } if ($emulate) { $this->copy($from, $to, $recursively); $this->delete($from, $recursively); } return $this; } /** * Copies a LDAP entry from one DN to another subtree. * * @param string|Zend_Ldap_Dn $from * @param string|Zend_Ldap_Dn $to * @param boolean $recursively * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function copyToSubtree($from, $to, $recursively = false) { if ($from instanceof Zend_Ldap_Dn) { $orgDnParts = $from->toArray(); } else { $orgDnParts = Zend_Ldap_Dn::explodeDn($from); } if ($to instanceof Zend_Ldap_Dn) { $newParentDnParts = $to->toArray(); } else { $newParentDnParts = Zend_Ldap_Dn::explodeDn($to); } $newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts); $newDn = Zend_Ldap_Dn::fromArray($newDnParts); return $this->copy($from, $newDn, $recursively); } /** * Copies a LDAP entry from one DN to another DN. * * @param string|Zend_Ldap_Dn $from * @param string|Zend_Ldap_Dn $to * @param boolean $recursively * @return Zend_Ldap Provides a fluid interface * @throws Zend_Ldap_Exception */ public function copy($from, $to, $recursively = false) { $entry = $this->getEntry($from, array(), true); if ($to instanceof Zend_Ldap_Dn) { $toDnParts = $to->toArray(); } else { $toDnParts = Zend_Ldap_Dn::explodeDn($to); } $this->add($to, $entry); if ($recursively === true && $this->countChildren($from)>0) { $children = $this->_getChildrenDns($from); foreach ($children as $c) { $cDnParts = Zend_Ldap_Dn::explodeDn($c); $newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts); $newChild = Zend_Ldap_Dn::implodeDn($newChildParts); $this->copy($c, $newChild, true); } } return $this; } /** * Returns the specified DN as a Zend_Ldap_Node * * @param string|Zend_Ldap_Dn $dn * @return Zend_Ldap_Node|null * @throws Zend_Ldap_Exception */ public function getNode($dn) { /** * Zend_Ldap_Node */ require_once 'Zend/Ldap/Node.php'; return Zend_Ldap_Node::fromLdap($dn, $this); } /** * Returns the base node as a Zend_Ldap_Node * * @return Zend_Ldap_Node * @throws Zend_Ldap_Exception */ public function getBaseNode() { return $this->getNode($this->getBaseDn(), $this); } /** * Returns the RootDSE * * @return Zend_Ldap_Node_RootDse * @throws Zend_Ldap_Exception */ public function getRootDse() { if ($this->_rootDse === null) { /** * @see Zend_Ldap_Node_Schema */ require_once 'Zend/Ldap/Node/RootDse.php'; $this->_rootDse = Zend_Ldap_Node_RootDse::create($this); } return $this->_rootDse; } /** * Returns the schema * * @return Zend_Ldap_Node_Schema * @throws Zend_Ldap_Exception */ public function getSchema() { if ($this->_schema === null) { /** * @see Zend_Ldap_Node_Schema */ require_once 'Zend/Ldap/Node/Schema.php'; $this->_schema = Zend_Ldap_Node_Schema::create($this); } return $this->_schema; } }