blob: 8ba0ef1de78b6d6e20241da01cac9477e195724f [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord;
4
5use Carbon\Carbon;
6use Closure;
7use LdapRecord\Auth\Guard;
8use LdapRecord\Configuration\DomainConfiguration;
9use LdapRecord\Events\DispatcherInterface;
10use LdapRecord\Query\Builder;
11use LdapRecord\Query\Cache;
12use Psr\SimpleCache\CacheInterface;
13
14class Connection
15{
16 use DetectsErrors;
17
18 /**
19 * The underlying LDAP connection.
20 *
21 * @var Ldap
22 */
23 protected $ldap;
24
25 /**
26 * The cache driver.
27 *
28 * @var Cache|null
29 */
30 protected $cache;
31
32 /**
33 * The domain configuration.
34 *
35 * @var DomainConfiguration
36 */
37 protected $configuration;
38
39 /**
40 * The event dispatcher;.
41 *
42 * @var DispatcherInterface|null
43 */
44 protected $dispatcher;
45
46 /**
47 * The current host connected to.
48 *
49 * @var string
50 */
51 protected $host;
52
53 /**
54 * The configured domain hosts.
55 *
56 * @var array
57 */
58 protected $hosts = [];
59
60 /**
61 * The attempted hosts that failed connecting to.
62 *
63 * @var array
64 */
65 protected $attempted = [];
66
67 /**
68 * The callback to execute upon total connection failure.
69 *
70 * @var Closure
71 */
72 protected $failed;
73
74 /**
75 * The authentication guard resolver.
76 *
77 * @var Closure
78 */
79 protected $authGuardResolver;
80
81 /**
82 * Whether the connection is retrying the initial connection attempt.
83 *
84 * @var bool
85 */
86 protected $retryingInitialConnection = false;
87
88 /**
89 * Constructor.
90 *
91 * @param array $config
92 * @param LdapInterface|null $ldap
93 */
94 public function __construct($config = [], LdapInterface $ldap = null)
95 {
96 $this->setConfiguration($config);
97
98 $this->setLdapConnection($ldap ?? new Ldap());
99
100 $this->failed = function () {
101 $this->dispatch(new Events\ConnectionFailed($this));
102 };
103
104 $this->authGuardResolver = function () {
105 return new Guard($this->ldap, $this->configuration);
106 };
107 }
108
109 /**
110 * Set the connection configuration.
111 *
112 * @param array $config
113 *
114 * @throws Configuration\ConfigurationException
115 *
116 * @return $this
117 */
118 public function setConfiguration($config = [])
119 {
120 $this->configuration = new DomainConfiguration($config);
121
122 $this->hosts = $this->configuration->get('hosts');
123
124 $this->host = reset($this->hosts);
125
126 return $this;
127 }
128
129 /**
130 * Set the LDAP connection.
131 *
132 * @param LdapInterface $ldap
133 *
134 * @return $this
135 */
136 public function setLdapConnection(LdapInterface $ldap)
137 {
138 $this->ldap = $ldap;
139
140 return $this;
141 }
142
143 /**
144 * Set the event dispatcher.
145 *
146 * @param DispatcherInterface $dispatcher
147 *
148 * @return $this
149 */
150 public function setDispatcher(DispatcherInterface $dispatcher)
151 {
152 $this->dispatcher = $dispatcher;
153
154 return $this;
155 }
156
157 /**
158 * Initializes the LDAP connection.
159 *
160 * @return void
161 */
162 public function initialize()
163 {
164 $this->configure();
165
166 $this->ldap->connect($this->host, $this->configuration->get('port'));
167 }
168
169 /**
170 * Configure the LDAP connection.
171 *
172 * @return void
173 */
174 protected function configure()
175 {
176 if ($this->configuration->get('use_ssl')) {
177 $this->ldap->ssl();
178 } elseif ($this->configuration->get('use_tls')) {
179 $this->ldap->tls();
180 }
181
182 $this->ldap->setOptions(array_replace(
183 $this->configuration->get('options'),
184 [
185 LDAP_OPT_PROTOCOL_VERSION => $this->configuration->get('version'),
186 LDAP_OPT_NETWORK_TIMEOUT => $this->configuration->get('timeout'),
187 LDAP_OPT_REFERRALS => $this->configuration->get('follow_referrals'),
188 ]
189 ));
190 }
191
192 /**
193 * Set the cache store.
194 *
195 * @param CacheInterface $store
196 *
197 * @return $this
198 */
199 public function setCache(CacheInterface $store)
200 {
201 $this->cache = new Cache($store);
202
203 return $this;
204 }
205
206 /**
207 * Get the cache store.
208 *
209 * @return Cache|null
210 */
211 public function getCache()
212 {
213 return $this->cache;
214 }
215
216 /**
217 * Get the LDAP configuration instance.
218 *
219 * @return DomainConfiguration
220 */
221 public function getConfiguration()
222 {
223 return $this->configuration;
224 }
225
226 /**
227 * Get the LDAP connection instance.
228 *
229 * @return Ldap
230 */
231 public function getLdapConnection()
232 {
233 return $this->ldap;
234 }
235
236 /**
237 * Bind to the LDAP server.
238 *
239 * If no username or password is specified, then the configured credentials are used.
240 *
241 * @param string|null $username
242 * @param string|null $password
243 *
244 * @throws Auth\BindException
245 * @throws LdapRecordException
246 *
247 * @return Connection
248 */
249 public function connect($username = null, $password = null)
250 {
251 $attempt = function () use ($username, $password) {
252 $this->dispatch(new Events\Connecting($this));
253
254 is_null($username) && is_null($password)
255 ? $this->auth()->bindAsConfiguredUser()
256 : $this->auth()->bind($username, $password);
257
258 $this->dispatch(new Events\Connected($this));
259
260 $this->retryingInitialConnection = false;
261 };
262
263 try {
264 $this->runOperationCallback($attempt);
265 } catch (LdapRecordException $e) {
266 $this->retryingInitialConnection = true;
267
268 $this->retryOnNextHost($e, $attempt);
269 }
270
271 return $this;
272 }
273
274 /**
275 * Reconnect to the LDAP server.
276 *
277 * @throws Auth\BindException
278 * @throws ConnectionException
279 *
280 * @return void
281 */
282 public function reconnect()
283 {
284 $this->reinitialize();
285
286 $this->connect();
287 }
288
289 /**
290 * Reinitialize the connection.
291 *
292 * @return void
293 */
294 protected function reinitialize()
295 {
296 $this->disconnect();
297
298 $this->initialize();
299 }
300
301 /**
302 * Disconnect from the LDAP server.
303 *
304 * @return void
305 */
306 public function disconnect()
307 {
308 $this->ldap->close();
309 }
310
311 /**
312 * Dispatch an event.
313 *
314 * @param object $event
315 *
316 * @return void
317 */
318 public function dispatch($event)
319 {
320 if (isset($this->dispatcher)) {
321 $this->dispatcher->dispatch($event);
322 }
323 }
324
325 /**
326 * Get the attempted hosts that failed connecting to.
327 *
328 * @return array
329 */
330 public function attempted()
331 {
332 return $this->attempted;
333 }
334
335 /**
336 * Perform the operation on the LDAP connection.
337 *
338 * @param Closure $operation
339 *
340 * @return mixed
341 */
342 public function run(Closure $operation)
343 {
344 try {
345 // Before running the operation, we will check if the current
346 // connection is bound and connect if necessary. Otherwise
347 // some LDAP operations will not be executed properly.
348 if (! $this->isConnected()) {
349 $this->connect();
350 }
351
352 return $this->runOperationCallback($operation);
353 } catch (LdapRecordException $e) {
354 if ($exception = $this->getExceptionForCauseOfFailure($e)) {
355 throw $exception;
356 }
357
358 return $this->tryAgainIfCausedByLostConnection($e, $operation);
359 }
360 }
361
362 /**
363 * Attempt to get an exception for the cause of failure.
364 *
365 * @param LdapRecordException $e
366 *
367 * @return mixed
368 */
369 protected function getExceptionForCauseOfFailure(LdapRecordException $e)
370 {
371 switch (true) {
372 case $this->errorContainsMessage($e->getMessage(), 'Already exists'):
373 return Exceptions\AlreadyExistsException::withDetailedError($e, $e->getDetailedError());
374 case $this->errorContainsMessage($e->getMessage(), 'Insufficient access'):
375 return Exceptions\InsufficientAccessException::withDetailedError($e, $e->getDetailedError());
376 case $this->errorContainsMessage($e->getMessage(), 'Constraint violation'):
377 return Exceptions\ConstraintViolationException::withDetailedError($e, $e->getDetailedError());
378 default:
379 return;
380 }
381 }
382
383 /**
384 * Run the operation callback on the current LDAP connection.
385 *
386 * @param Closure $operation
387 *
388 * @throws LdapRecordException
389 *
390 * @return mixed
391 */
392 protected function runOperationCallback(Closure $operation)
393 {
394 return $operation($this->ldap);
395 }
396
397 /**
398 * Get a new auth guard instance.
399 *
400 * @return Auth\Guard
401 */
402 public function auth()
403 {
404 if (! $this->ldap->isConnected()) {
405 $this->initialize();
406 }
407
408 $guard = call_user_func($this->authGuardResolver);
409
410 $guard->setDispatcher(
411 Container::getInstance()->getEventDispatcher()
412 );
413
414 return $guard;
415 }
416
417 /**
418 * Get a new query builder for the connection.
419 *
420 * @return Query\Builder
421 */
422 public function query()
423 {
424 return (new Builder($this))
425 ->setCache($this->cache)
426 ->setBaseDn($this->configuration->get('base_dn'));
427 }
428
429 /**
430 * Determine if the LDAP connection is bound.
431 *
432 * @return bool
433 */
434 public function isConnected()
435 {
436 return $this->ldap->isBound();
437 }
438
439 /**
440 * Attempt to retry an LDAP operation if due to a lost connection.
441 *
442 * @param LdapRecordException $e
443 * @param Closure $operation
444 *
445 * @throws LdapRecordException
446 *
447 * @return mixed
448 */
449 protected function tryAgainIfCausedByLostConnection(LdapRecordException $e, Closure $operation)
450 {
451 // If the operation failed due to a lost or failed connection,
452 // we'll attempt reconnecting and running the operation again
453 // underneath the same host, and then move onto the next.
454 if ($this->causedByLostConnection($e->getMessage())) {
455 return $this->retry($operation);
456 }
457
458 throw $e;
459 }
460
461 /**
462 * Retry the operation on the current host.
463 *
464 * @param Closure $operation
465 *
466 * @throws LdapRecordException
467 *
468 * @return mixed
469 */
470 protected function retry(Closure $operation)
471 {
472 try {
473 $this->retryingInitialConnection
474 ? $this->reinitialize()
475 : $this->reconnect();
476
477 return $this->runOperationCallback($operation);
478 } catch (LdapRecordException $e) {
479 return $this->retryOnNextHost($e, $operation);
480 }
481 }
482
483 /**
484 * Attempt the operation again on the next host.
485 *
486 * @param LdapRecordException $e
487 * @param Closure $operation
488 *
489 * @throws LdapRecordException
490 *
491 * @return mixed
492 */
493 protected function retryOnNextHost(LdapRecordException $e, Closure $operation)
494 {
495 $this->attempted[$this->host] = Carbon::now();
496
497 if (($key = array_search($this->host, $this->hosts)) !== false) {
498 unset($this->hosts[$key]);
499 }
500
501 if ($next = reset($this->hosts)) {
502 $this->host = $next;
503
504 return $this->tryAgainIfCausedByLostConnection($e, $operation);
505 }
506
507 call_user_func($this->failed, $this->ldap);
508
509 throw $e;
510 }
511}