2023-03-11 12:04:29 +03:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Zend Framework (http://framework.zend.com/)
|
|
|
|
*
|
|
|
|
* @link http://github.com/zendframework/zf2 for the canonical source repository
|
2023-04-01 09:03:34 +03:00
|
|
|
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
|
2023-03-11 12:04:29 +03:00
|
|
|
* @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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-01 09:03:34 +03:00
|
|
|
return;
|
2023-03-11 12:04:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|