torrentpier-lts/library/Zend/Ldap/Ldap.php

1538 lines
47 KiB
PHP

<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Ldap;
use Traversable;
use Zend\Stdlib\ErrorHandler;
class 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 bool|null|string
*/
protected $boundUser = false;
/**
* Caches the RootDse
*
* @var Node\RootDse
*/
protected $rootDse = null;
/**
* Caches the schema
*
* @var Node\Schema
*/
protected $schema = null;
/**
* Constructor.
*
* @param array|Traversable $options Options used in connecting, binding, etc.
* @throws Exception\LdapException
*/
public function __construct($options = array())
{
if (!extension_loaded('ldap')) {
throw new Exception\LdapException(null, 'LDAP extension not loaded',
Exception\LdapException::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()
{
ErrorHandler::start(E_WARNING);
$ret = ldap_get_option($this->resource, LDAP_OPT_ERROR_NUMBER, $err);
ErrorHandler::stop();
if ($ret === true) {
if ($err <= -1 && $err >= -17) {
/* For some reason draft-ietf-ldapext-ldap-c-api-xx.txt error
* codes in OpenLDAP are negative values from -1 to -17.
*/
$err = Exception\LdapException::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.
*/
ErrorHandler::start(E_WARNING);
$estr1 = ldap_error($this->resource);
ErrorHandler::stop();
if ($errorCode !== 0 && $estr1 === 'Success') {
ErrorHandler::start(E_WARNING);
$estr1 = ldap_err2str($errorCode);
ErrorHandler::stop();
}
if (!empty($estr1)) {
$errorMessages[] = $estr1;
}
ErrorHandler::start(E_WARNING);
ldap_get_option($this->resource, LDAP_OPT_ERROR_STRING, $estr2);
ErrorHandler::stop();
if (!empty($estr2) && !in_array($estr2, $errorMessages)) {
$errorMessages[] = $estr2;
}
$message = '';
if ($errorCode > 0) {
$message = '0x' . dechex($errorCode) . ' ';
}
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 bool|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
* optReferrals
* tryUsernameSplit
* networkTimeout
*
* @param array|Traversable $options Options used in connecting, binding, etc.
* @return Ldap Provides a fluent interface
* @throws Exception\LdapException
*/
public function setOptions($options)
{
if ($options instanceof Traversable) {
$options = iterator_to_array($options);
}
$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,
'networkTimeout' => null,
);
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\Reader\Ini
* returning '1' as a string (ZF-3163).
*/
switch ($key) {
case 'port':
case 'accountCanonicalForm':
case 'networkTimeout':
$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);
throw new Exception\LdapException(null, "Unknown Zend\\Ldap\\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 bool 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 bool 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 int 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 = self::ACCTNAME_FORM_BACKSLASH;
} else {
if ($accountDomainName) {
$accountCanonicalForm = self::ACCTNAME_FORM_PRINCIPAL;
} else {
$accountCanonicalForm = self::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 bool Allow empty passwords
*/
protected function getAllowEmptyPassword()
{
return $this->options['allowEmptyPassword'];
}
/**
* @return bool The default SSL / TLS encrypted transport control
*/
protected function getUseStartTls()
{
return $this->options['useStartTls'];
}
/**
* @return bool Opt. Referrals
*/
protected function getOptReferrals()
{
return $this->options['optReferrals'];
}
/**
* @return bool Try splitting the username into username and domain
*/
protected function getTryUsernameSplit()
{
return $this->options['tryUsernameSplit'];
}
/**
* @return int The value for network timeout when connect to the LDAP server.
*/
protected function getNetworkTimeout()
{
return $this->options['networkTimeout'];
}
/**
* @param string $acctname
* @return string The LDAP search filter for matching directory accounts
*/
protected function getAccountFilter($acctname)
{
$dname = '';
$aname = '';
$this->splitName($acctname, $dname, $aname);
$accountFilterFormat = $this->getAccountFilterFormat();
$aname = Filter\AbstractFilter::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 Exception\LdapException
*/
protected function getAccountDn($acctname)
{
if (Dn::checkDn($acctname)) {
return $acctname;
}
$acctname = $this->getCanonicalAccountName($acctname, self::ACCTNAME_FORM_USERNAME);
$acct = $this->getAccount($acctname, array('dn'));
return $acct['dn'];
}
/**
* @param string $dname The domain name to check
* @return bool
*/
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 $form The desired form of canonicalization
* @return string The canonicalized name in the desired form
* @throws Exception\LdapException
*/
public function getCanonicalAccountName($acctname, $form = 0)
{
$dname = '';
$uname = '';
$this->splitName($acctname, $dname, $uname);
if (!$this->isPossibleAuthority($dname)) {
throw new Exception\LdapException(null,
"Binding domain is not an authority for user: $acctname",
Exception\LdapException::LDAP_X_DOMAIN_MISMATCH);
}
if (!$uname) {
throw new Exception\LdapException(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 self::ACCTNAME_FORM_DN:
return $this->getAccountDn($acctname);
case self::ACCTNAME_FORM_USERNAME:
return $uname;
case self::ACCTNAME_FORM_BACKSLASH:
$accountDomainNameShort = $this->getAccountDomainNameShort();
if (!$accountDomainNameShort) {
throw new Exception\LdapException(null, 'Option required: accountDomainNameShort');
}
return "$accountDomainNameShort\\$uname";
case self::ACCTNAME_FORM_PRINCIPAL:
$accountDomainName = $this->getAccountDomainName();
if (!$accountDomainName) {
throw new Exception\LdapException(null, 'Option required: accountDomainName');
}
return "$uname@$accountDomainName";
default:
throw new Exception\LdapException(null, "Unknown canonical name form: $form");
}
}
/**
* @param string $acctname
* @param array $attrs An array of names of desired attributes
* @return array An array of the attributes representing the account
* @throws Exception\LdapException
*/
protected function getAccount($acctname, array $attrs = null)
{
$baseDn = $this->getBaseDn();
if (!$baseDn) {
throw new Exception\LdapException(null, 'Base DN not set');
}
$accountFilter = $this->getAccountFilter($acctname);
if (!$accountFilter) {
throw new Exception\LdapException(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) {
$code = Exception\LdapException::LDAP_NO_SUCH_OBJECT;
$str = "No object found for: $accountFilter";
} else {
$code = Exception\LdapException::LDAP_OPERATIONS_ERROR;
$str = "Unexpected result count ($count) for: $accountFilter";
}
}
$accounts->close();
throw new Exception\LdapException($this, $str, $code);
}
/**
* @return Ldap Provides a fluent interface
*/
public function disconnect()
{
if (is_resource($this->resource)) {
ErrorHandler::start(E_WARNING);
ldap_unbind($this->resource);
ErrorHandler::stop();
}
$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 bool $useSsl Use SSL
* @param bool $useStartTls Use STARTTLS
* @param int $networkTimeout The value for network timeout when connect to the LDAP server.
* @return Ldap Provides a fluent interface
* @throws Exception\LdapException
*/
public function connect($host = null, $port = null, $useSsl = null, $useStartTls = null, $networkTimeout = 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 ($networkTimeout === null) {
$networkTimeout = $this->getNetworkTimeout();
} else {
$networkTimeout = (int) $networkTimeout;
}
if (!$host) {
throw new Exception\LdapException(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.
*/
ErrorHandler::start();
$resource = ($useUri) ? ldap_connect($this->connectString) : ldap_connect($host, $port);
ErrorHandler::stop();
if (is_resource($resource) === true) {
$this->resource = $resource;
$this->boundUser = false;
$optReferrals = ($this->getOptReferrals()) ? 1 : 0;
ErrorHandler::start(E_WARNING);
if (ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3)
&& ldap_set_option($resource, LDAP_OPT_REFERRALS, $optReferrals)
) {
if ($networkTimeout) {
ldap_set_option($resource, LDAP_OPT_NETWORK_TIMEOUT, $networkTimeout);
}
if ($useSsl || !$useStartTls || ldap_start_tls($resource)) {
ErrorHandler::stop();
return $this;
}
}
ErrorHandler::stop();
$zle = new Exception\LdapException($this, "$host:$port");
$this->disconnect();
throw $zle;
}
throw new Exception\LdapException(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 Ldap Provides a fluent interface
* @throws Exception\LdapException
*/
public function bind($username = null, $password = null)
{
$moreCreds = true;
// Security check: remove null bytes in password
// @see https://net.educause.edu/ir/library/pdf/csd4875.pdf
$password = str_replace("\0", '', $password);
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.
*/
if (!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 (Exception\LdapException $zle) {
switch ($zle->getCode()) {
case Exception\LdapException::LDAP_NO_SUCH_OBJECT:
case Exception\LdapException::LDAP_X_DOMAIN_MISMATCH:
case Exception\LdapException::LDAP_X_EXTENSION_NOT_LOADED:
throw $zle;
}
throw new Exception\LdapException(null,
'Failed to retrieve DN for account: ' . $username .
' [' . $zle->getMessage() . ']',
Exception\LdapException::LDAP_OPERATIONS_ERROR);
}
} else {
throw new Exception\LdapException(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) {
$zle = new Exception\LdapException(null,
'Empty password not allowed - see allowEmptyPassword option.');
} else {
ErrorHandler::start(E_WARNING);
$bind = ldap_bind($this->resource, $username, $password);
ErrorHandler::stop();
if ($bind) {
$this->boundUser = $username;
return $this;
}
$message = ($username === null) ? $this->connectString : $username;
switch ($this->getLastErrorCode()) {
case Exception\LdapException::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 Exception\LdapException($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|Filter\AbstractFilter|array $filter
* @param string|Dn|null $basedn
* @param int $scope
* @param array $attributes
* @param string|null $sort
* @param string|null $collectionClass
* @param int $sizelimit
* @param int $timelimit
* @return Collection
* @throws Exception\LdapException
*/
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;
break;
}
}
}
if ($basedn === null) {
$basedn = $this->getBaseDn();
} elseif ($basedn instanceof Dn) {
$basedn = $basedn->toString();
}
if ($filter instanceof Filter\AbstractFilter) {
$filter = $filter->toString();
}
$resource = $this->getResource();
ErrorHandler::start(E_WARNING);
switch ($scope) {
case self::SEARCH_SCOPE_ONE:
$search = ldap_list($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
break;
case self::SEARCH_SCOPE_BASE:
$search = ldap_read($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
break;
case self::SEARCH_SCOPE_SUB:
default:
$search = ldap_search($resource, $basedn, $filter, $attributes, 0, $sizelimit, $timelimit);
break;
}
ErrorHandler::stop();
if ($search === false) {
throw new Exception\LdapException($this, 'searching: ' . $filter);
}
if ($sort !== null && is_string($sort)) {
ErrorHandler::start(E_WARNING);
$isSorted = ldap_sort($resource, $search, $sort);
ErrorHandler::stop();
if ($isSorted === false) {
throw new Exception\LdapException($this, 'sorting: ' . $sort);
}
}
$iterator = new Collection\DefaultIterator($this, $search);
return $this->createCollection($iterator, $collectionClass);
}
/**
* Extension point for collection creation
*
* @param Collection\DefaultIterator $iterator
* @param string|null $collectionClass
* @return Collection
* @throws Exception\LdapException
*/
protected function createCollection(Collection\DefaultIterator $iterator, $collectionClass)
{
if ($collectionClass === null) {
return new Collection($iterator);
} else {
$collectionClass = (string) $collectionClass;
if (!class_exists($collectionClass)) {
throw new Exception\LdapException(null,
"Class '$collectionClass' can not be found");
}
if (!is_subclass_of($collectionClass, 'Zend\Ldap\Collection')) {
throw new Exception\LdapException(null,
"Class '$collectionClass' must subclass 'Zend\\Ldap\\Collection'");
}
return new $collectionClass($iterator);
}
}
/**
* Count items found by given filter.
*
* @param string|Filter\AbstractFilter $filter
* @param string|Dn|null $basedn
* @param int $scope
* @return int
* @throws Exception\LdapException
*/
public function count($filter, $basedn = null, $scope = self::SEARCH_SCOPE_SUB)
{
try {
$result = $this->search($filter, $basedn, $scope, array('dn'), null);
} catch (Exception\LdapException $e) {
if ($e->getCode() === Exception\LdapException::LDAP_NO_SUCH_OBJECT) {
return 0;
}
throw $e;
}
return $result->count();
}
/**
* Count children for a given DN.
*
* @param string|Dn $dn
* @return int
* @throws Exception\LdapException
*/
public function countChildren($dn)
{
return $this->count('(objectClass=*)', $dn, self::SEARCH_SCOPE_ONE);
}
/**
* Check if a given DN exists.
*
* @param string|Dn $dn
* @return bool
* @throws Exception\LdapException
*/
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|Filter\AbstractFilter|array $filter
* @param string|Dn|null $basedn
* @param int $scope
* @param array $attributes
* @param string|null $sort
* @param bool $reverseSort
* @param int $sizelimit
* @param int $timelimit
* @return array
* @throws Exception\LdapException
*/
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|Dn $dn
* @param array $attributes
* @param bool $throwOnNotFound
* @return array
* @throws null|Exception\LdapException
*/
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 (Exception\LdapException $e) {
if ($throwOnNotFound !== false) {
throw $e;
}
}
return;
}
/**
* Prepares an ldap data entry array for insert/update operation
*
* @param array $entry
* @throws Exception\InvalidArgumentException
* @return void
*/
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]);
} elseif (!is_scalar($v)) {
throw new Exception\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();
} elseif (!is_scalar($value)) {
throw new Exception\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|Dn $dn
* @param array $entry
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function add($dn, array $entry)
{
if (!($dn instanceof Dn)) {
$dn = Dn::factory($dn, null);
}
static::prepareLdapEntryArray($entry);
foreach ($entry as $key => $value) {
if (is_array($value) && count($value) === 0) {
unset($entry[$key]);
}
}
$rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER);
foreach ($rdnParts as $key => $value) {
$value = Dn::unescapeValue($value);
if (!array_key_exists($key, $entry)) {
$entry[$key] = array($value);
} elseif (!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]);
}
}
$resource = $this->getResource();
ErrorHandler::start(E_WARNING);
$isAdded = ldap_add($resource, $dn->toString(), $entry);
ErrorHandler::stop();
if ($isAdded === false) {
throw new Exception\LdapException($this, 'adding: ' . $dn->toString());
}
return $this;
}
/**
* Update LDAP registry
*
* @param string|Dn $dn
* @param array $entry
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function update($dn, array $entry)
{
if (!($dn instanceof Dn)) {
$dn = Dn::factory($dn, null);
}
static::prepareLdapEntryArray($entry);
$rdnParts = $dn->getRdn(Dn::ATTR_CASEFOLD_LOWER);
foreach ($rdnParts as $key => $value) {
$value = 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) {
$resource = $this->getResource();
ErrorHandler::start(E_WARNING);
$isModified = ldap_modify($resource, $dn->toString(), $entry);
ErrorHandler::stop();
if ($isModified === false) {
throw new Exception\LdapException($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|Dn $dn
* @param array $entry
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function save($dn, array $entry)
{
if ($dn instanceof 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|Dn $dn
* @param bool $recursively
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function delete($dn, $recursively = false)
{
if ($dn instanceof Dn) {
$dn = $dn->toString();
}
if ($recursively === true) {
if ($this->countChildren($dn) > 0) {
$children = $this->getChildrenDns($dn);
foreach ($children as $c) {
$this->delete($c, true);
}
}
}
$resource = $this->getResource();
ErrorHandler::start(E_WARNING);
$isDeleted = ldap_delete($resource, $dn);
ErrorHandler::stop();
if ($isDeleted === false) {
throw new Exception\LdapException($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|Dn $parentDn
* @throws Exception\LdapException
* @return array of DNs
*/
protected function getChildrenDns($parentDn)
{
if ($parentDn instanceof Dn) {
$parentDn = $parentDn->toString();
}
$children = array();
$resource = $this->getResource();
ErrorHandler::start(E_WARNING);
$search = ldap_list($resource, $parentDn, '(objectClass=*)', array('dn'));
for (
$entry = ldap_first_entry($resource, $search);
$entry !== false;
$entry = ldap_next_entry($resource, $entry)
) {
$childDn = ldap_get_dn($resource, $entry);
if ($childDn === false) {
ErrorHandler::stop();
throw new Exception\LdapException($this, 'getting dn');
}
$children[] = $childDn;
}
ldap_free_result($search);
ErrorHandler::stop();
return $children;
}
/**
* Moves a LDAP entry from one DN to another subtree.
*
* @param string|Dn $from
* @param string|Dn $to
* @param bool $recursively
* @param bool $alwaysEmulate
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function moveToSubtree($from, $to, $recursively = false, $alwaysEmulate = false)
{
if ($from instanceof Dn) {
$orgDnParts = $from->toArray();
} else {
$orgDnParts = Dn::explodeDn($from);
}
if ($to instanceof Dn) {
$newParentDnParts = $to->toArray();
} else {
$newParentDnParts = Dn::explodeDn($to);
}
$newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
$newDn = 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|Dn $from
* @param string|Dn $to
* @param bool $recursively
* @param bool $alwaysEmulate
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
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 implicitly moves the entry to another location within the tree.
*
* @param string|Dn $from
* @param string|Dn $to
* @param bool $recursively
* @param bool $alwaysEmulate
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function rename($from, $to, $recursively = false, $alwaysEmulate = false)
{
$emulate = (bool) $alwaysEmulate;
if (!function_exists('ldap_rename')) {
$emulate = true;
} elseif ($recursively) {
$emulate = true;
}
if ($emulate === false) {
if ($from instanceof Dn) {
$from = $from->toString();
}
if ($to instanceof Dn) {
$newDnParts = $to->toArray();
} else {
$newDnParts = Dn::explodeDn($to);
}
$newRdn = Dn::implodeRdn(array_shift($newDnParts));
$newParent = Dn::implodeDn($newDnParts);
$resource = $this->getResource();
ErrorHandler::start(E_WARNING);
$isOK = ldap_rename($resource, $from, $newRdn, $newParent, true);
ErrorHandler::stop();
if ($isOK === false) {
throw new Exception\LdapException($this, 'renaming ' . $from . ' to ' . $to);
} elseif (!$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|Dn $from
* @param string|Dn $to
* @param bool $recursively
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function copyToSubtree($from, $to, $recursively = false)
{
if ($from instanceof Dn) {
$orgDnParts = $from->toArray();
} else {
$orgDnParts = Dn::explodeDn($from);
}
if ($to instanceof Dn) {
$newParentDnParts = $to->toArray();
} else {
$newParentDnParts = Dn::explodeDn($to);
}
$newDnParts = array_merge(array(array_shift($orgDnParts)), $newParentDnParts);
$newDn = Dn::fromArray($newDnParts);
return $this->copy($from, $newDn, $recursively);
}
/**
* Copies a LDAP entry from one DN to another DN.
*
* @param string|Dn $from
* @param string|Dn $to
* @param bool $recursively
* @return Ldap Provides a fluid interface
* @throws Exception\LdapException
*/
public function copy($from, $to, $recursively = false)
{
$entry = $this->getEntry($from, array(), true);
if ($to instanceof Dn) {
$toDnParts = $to->toArray();
} else {
$toDnParts = Dn::explodeDn($to);
}
$this->add($to, $entry);
if ($recursively === true && $this->countChildren($from) > 0) {
$children = $this->getChildrenDns($from);
foreach ($children as $c) {
$cDnParts = Dn::explodeDn($c);
$newChildParts = array_merge(array(array_shift($cDnParts)), $toDnParts);
$newChild = Dn::implodeDn($newChildParts);
$this->copy($c, $newChild, true);
}
}
return $this;
}
/**
* Returns the specified DN as a Zend\Ldap\Node
*
* @param string|Dn $dn
* @return Node|null
* @throws Exception\LdapException
*/
public function getNode($dn)
{
return Node::fromLdap($dn, $this);
}
/**
* Returns the base node as a Zend\Ldap\Node
*
* @return Node
* @throws Exception\LdapException
*/
public function getBaseNode()
{
return $this->getNode($this->getBaseDn(), $this);
}
/**
* Returns the RootDse
*
* @return Node\RootDse
* @throws Exception\LdapException
*/
public function getRootDse()
{
if ($this->rootDse === null) {
$this->rootDse = Node\RootDse::create($this);
}
return $this->rootDse;
}
/**
* Returns the schema
*
* @return Node\Schema
* @throws Exception\LdapException
*/
public function getSchema()
{
if ($this->schema === null) {
$this->schema = Node\Schema::create($this);
}
return $this->schema;
}
}