| <?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; |
| } |
| } |