blob: 8ba0ef1de78b6d6e20241da01cac9477e195724f [file] [log] [blame]
<?php
namespace LdapRecord;
use Carbon\Carbon;
use Closure;
use LdapRecord\Auth\Guard;
use LdapRecord\Configuration\DomainConfiguration;
use LdapRecord\Events\DispatcherInterface;
use LdapRecord\Query\Builder;
use LdapRecord\Query\Cache;
use Psr\SimpleCache\CacheInterface;
class Connection
{
use DetectsErrors;
/**
* The underlying LDAP connection.
*
* @var Ldap
*/
protected $ldap;
/**
* The cache driver.
*
* @var Cache|null
*/
protected $cache;
/**
* The domain configuration.
*
* @var DomainConfiguration
*/
protected $configuration;
/**
* The event dispatcher;.
*
* @var DispatcherInterface|null
*/
protected $dispatcher;
/**
* The current host connected to.
*
* @var string
*/
protected $host;
/**
* The configured domain hosts.
*
* @var array
*/
protected $hosts = [];
/**
* The attempted hosts that failed connecting to.
*
* @var array
*/
protected $attempted = [];
/**
* The callback to execute upon total connection failure.
*
* @var Closure
*/
protected $failed;
/**
* The authentication guard resolver.
*
* @var Closure
*/
protected $authGuardResolver;
/**
* Whether the connection is retrying the initial connection attempt.
*
* @var bool
*/
protected $retryingInitialConnection = false;
/**
* Constructor.
*
* @param array $config
* @param LdapInterface|null $ldap
*/
public function __construct($config = [], LdapInterface $ldap = null)
{
$this->setConfiguration($config);
$this->setLdapConnection($ldap ?? new Ldap());
$this->failed = function () {
$this->dispatch(new Events\ConnectionFailed($this));
};
$this->authGuardResolver = function () {
return new Guard($this->ldap, $this->configuration);
};
}
/**
* Set the connection configuration.
*
* @param array $config
*
* @throws Configuration\ConfigurationException
*
* @return $this
*/
public function setConfiguration($config = [])
{
$this->configuration = new DomainConfiguration($config);
$this->hosts = $this->configuration->get('hosts');
$this->host = reset($this->hosts);
return $this;
}
/**
* Set the LDAP connection.
*
* @param LdapInterface $ldap
*
* @return $this
*/
public function setLdapConnection(LdapInterface $ldap)
{
$this->ldap = $ldap;
return $this;
}
/**
* Set the event dispatcher.
*
* @param DispatcherInterface $dispatcher
*
* @return $this
*/
public function setDispatcher(DispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
return $this;
}
/**
* Initializes the LDAP connection.
*
* @return void
*/
public function initialize()
{
$this->configure();
$this->ldap->connect($this->host, $this->configuration->get('port'));
}
/**
* Configure the LDAP connection.
*
* @return void
*/
protected function configure()
{
if ($this->configuration->get('use_ssl')) {
$this->ldap->ssl();
} elseif ($this->configuration->get('use_tls')) {
$this->ldap->tls();
}
$this->ldap->setOptions(array_replace(
$this->configuration->get('options'),
[
LDAP_OPT_PROTOCOL_VERSION => $this->configuration->get('version'),
LDAP_OPT_NETWORK_TIMEOUT => $this->configuration->get('timeout'),
LDAP_OPT_REFERRALS => $this->configuration->get('follow_referrals'),
]
));
}
/**
* Set the cache store.
*
* @param CacheInterface $store
*
* @return $this
*/
public function setCache(CacheInterface $store)
{
$this->cache = new Cache($store);
return $this;
}
/**
* Get the cache store.
*
* @return Cache|null
*/
public function getCache()
{
return $this->cache;
}
/**
* Get the LDAP configuration instance.
*
* @return DomainConfiguration
*/
public function getConfiguration()
{
return $this->configuration;
}
/**
* Get the LDAP connection instance.
*
* @return Ldap
*/
public function getLdapConnection()
{
return $this->ldap;
}
/**
* Bind to the LDAP server.
*
* If no username or password is specified, then the configured credentials are used.
*
* @param string|null $username
* @param string|null $password
*
* @throws Auth\BindException
* @throws LdapRecordException
*
* @return Connection
*/
public function connect($username = null, $password = null)
{
$attempt = function () use ($username, $password) {
$this->dispatch(new Events\Connecting($this));
is_null($username) && is_null($password)
? $this->auth()->bindAsConfiguredUser()
: $this->auth()->bind($username, $password);
$this->dispatch(new Events\Connected($this));
$this->retryingInitialConnection = false;
};
try {
$this->runOperationCallback($attempt);
} catch (LdapRecordException $e) {
$this->retryingInitialConnection = true;
$this->retryOnNextHost($e, $attempt);
}
return $this;
}
/**
* Reconnect to the LDAP server.
*
* @throws Auth\BindException
* @throws ConnectionException
*
* @return void
*/
public function reconnect()
{
$this->reinitialize();
$this->connect();
}
/**
* Reinitialize the connection.
*
* @return void
*/
protected function reinitialize()
{
$this->disconnect();
$this->initialize();
}
/**
* Disconnect from the LDAP server.
*
* @return void
*/
public function disconnect()
{
$this->ldap->close();
}
/**
* Dispatch an event.
*
* @param object $event
*
* @return void
*/
public function dispatch($event)
{
if (isset($this->dispatcher)) {
$this->dispatcher->dispatch($event);
}
}
/**
* Get the attempted hosts that failed connecting to.
*
* @return array
*/
public function attempted()
{
return $this->attempted;
}
/**
* Perform the operation on the LDAP connection.
*
* @param Closure $operation
*
* @return mixed
*/
public function run(Closure $operation)
{
try {
// Before running the operation, we will check if the current
// connection is bound and connect if necessary. Otherwise
// some LDAP operations will not be executed properly.
if (! $this->isConnected()) {
$this->connect();
}
return $this->runOperationCallback($operation);
} catch (LdapRecordException $e) {
if ($exception = $this->getExceptionForCauseOfFailure($e)) {
throw $exception;
}
return $this->tryAgainIfCausedByLostConnection($e, $operation);
}
}
/**
* Attempt to get an exception for the cause of failure.
*
* @param LdapRecordException $e
*
* @return mixed
*/
protected function getExceptionForCauseOfFailure(LdapRecordException $e)
{
switch (true) {
case $this->errorContainsMessage($e->getMessage(), 'Already exists'):
return Exceptions\AlreadyExistsException::withDetailedError($e, $e->getDetailedError());
case $this->errorContainsMessage($e->getMessage(), 'Insufficient access'):
return Exceptions\InsufficientAccessException::withDetailedError($e, $e->getDetailedError());
case $this->errorContainsMessage($e->getMessage(), 'Constraint violation'):
return Exceptions\ConstraintViolationException::withDetailedError($e, $e->getDetailedError());
default:
return;
}
}
/**
* Run the operation callback on the current LDAP connection.
*
* @param Closure $operation
*
* @throws LdapRecordException
*
* @return mixed
*/
protected function runOperationCallback(Closure $operation)
{
return $operation($this->ldap);
}
/**
* Get a new auth guard instance.
*
* @return Auth\Guard
*/
public function auth()
{
if (! $this->ldap->isConnected()) {
$this->initialize();
}
$guard = call_user_func($this->authGuardResolver);
$guard->setDispatcher(
Container::getInstance()->getEventDispatcher()
);
return $guard;
}
/**
* Get a new query builder for the connection.
*
* @return Query\Builder
*/
public function query()
{
return (new Builder($this))
->setCache($this->cache)
->setBaseDn($this->configuration->get('base_dn'));
}
/**
* Determine if the LDAP connection is bound.
*
* @return bool
*/
public function isConnected()
{
return $this->ldap->isBound();
}
/**
* Attempt to retry an LDAP operation if due to a lost connection.
*
* @param LdapRecordException $e
* @param Closure $operation
*
* @throws LdapRecordException
*
* @return mixed
*/
protected function tryAgainIfCausedByLostConnection(LdapRecordException $e, Closure $operation)
{
// If the operation failed due to a lost or failed connection,
// we'll attempt reconnecting and running the operation again
// underneath the same host, and then move onto the next.
if ($this->causedByLostConnection($e->getMessage())) {
return $this->retry($operation);
}
throw $e;
}
/**
* Retry the operation on the current host.
*
* @param Closure $operation
*
* @throws LdapRecordException
*
* @return mixed
*/
protected function retry(Closure $operation)
{
try {
$this->retryingInitialConnection
? $this->reinitialize()
: $this->reconnect();
return $this->runOperationCallback($operation);
} catch (LdapRecordException $e) {
return $this->retryOnNextHost($e, $operation);
}
}
/**
* Attempt the operation again on the next host.
*
* @param LdapRecordException $e
* @param Closure $operation
*
* @throws LdapRecordException
*
* @return mixed
*/
protected function retryOnNextHost(LdapRecordException $e, Closure $operation)
{
$this->attempted[$this->host] = Carbon::now();
if (($key = array_search($this->host, $this->hosts)) !== false) {
unset($this->hosts[$key]);
}
if ($next = reset($this->hosts)) {
$this->host = $next;
return $this->tryAgainIfCausedByLostConnection($e, $operation);
}
call_user_func($this->failed, $this->ldap);
throw $e;
}
}