<?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-2012 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
* @version $Id: Ldap.php 24594 2012-01-05 21:27:01Z matthew $
*/
/**
* @category Zend
* @package Zend_Ldap
* @copyright Copyright (c) 2005-2012 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;
}
}