Completed
Push — master ( 99d627...5060d6 )
by Blizzz
22:19
created
apps/user_ldap/lib/Connection.php 1 patch
Indentation   +671 added lines, -671 removed lines patch added patch discarded remove patch
@@ -94,675 +94,675 @@
 block discarded – undo
94 94
  * @property string $ldapAttributePronouns
95 95
  */
96 96
 class Connection extends LDAPUtility {
97
-	private ?\LDAP\Connection $ldapConnectionRes = null;
98
-	private bool $configured = false;
99
-
100
-	/**
101
-	 * @var bool whether connection should be kept on __destruct
102
-	 */
103
-	private bool $dontDestruct = false;
104
-
105
-	/**
106
-	 * @var bool runtime flag that indicates whether supported primary groups are available
107
-	 */
108
-	public $hasPrimaryGroups = true;
109
-
110
-	/**
111
-	 * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
112
-	 */
113
-	public $hasGidNumber = true;
114
-
115
-	/**
116
-	 * @var ICache|null
117
-	 */
118
-	protected $cache = null;
119
-
120
-	/** @var Configuration settings handler * */
121
-	protected $configuration;
122
-
123
-	/**
124
-	 * @var bool
125
-	 */
126
-	protected $doNotValidate = false;
127
-
128
-	/**
129
-	 * @var bool
130
-	 */
131
-	protected $ignoreValidation = false;
132
-
133
-	/**
134
-	 * @var array{sum?: string, result?: bool}
135
-	 */
136
-	protected $bindResult = [];
137
-
138
-	protected LoggerInterface $logger;
139
-	private IL10N $l10n;
140
-
141
-	/**
142
-	 * Constructor
143
-	 * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
144
-	 * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
145
-	 */
146
-	public function __construct(
147
-		ILDAPWrapper $ldap,
148
-		private string $configPrefix = '',
149
-		private ?string $configID = 'user_ldap',
150
-	) {
151
-		parent::__construct($ldap);
152
-		$this->configuration = new Configuration($this->configPrefix, !is_null($this->configID));
153
-		$memcache = Server::get(ICacheFactory::class);
154
-		if ($memcache->isAvailable()) {
155
-			$this->cache = $memcache->createDistributed();
156
-		}
157
-		$helper = Server::get(Helper::class);
158
-		$this->doNotValidate = !in_array($this->configPrefix,
159
-			$helper->getServerConfigurationPrefixes());
160
-		$this->logger = Server::get(LoggerInterface::class);
161
-		$this->l10n = Util::getL10N('user_ldap');
162
-	}
163
-
164
-	public function __destruct() {
165
-		if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
166
-			@$this->ldap->unbind($this->ldapConnectionRes);
167
-			$this->bindResult = [];
168
-		}
169
-	}
170
-
171
-	/**
172
-	 * defines behaviour when the instance is cloned
173
-	 */
174
-	public function __clone() {
175
-		$this->configuration = new Configuration($this->configPrefix,
176
-			!is_null($this->configID));
177
-		if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
178
-			$this->bindResult = [];
179
-		}
180
-		$this->ldapConnectionRes = null;
181
-		$this->dontDestruct = true;
182
-	}
183
-
184
-	public function __get(string $name) {
185
-		if (!$this->configured) {
186
-			$this->readConfiguration();
187
-		}
188
-
189
-		return $this->configuration->$name;
190
-	}
191
-
192
-	/**
193
-	 * @param string $name
194
-	 * @param mixed $value
195
-	 */
196
-	public function __set($name, $value) {
197
-		$this->doNotValidate = false;
198
-		$before = $this->configuration->$name;
199
-		$this->configuration->$name = $value;
200
-		$after = $this->configuration->$name;
201
-		if ($before !== $after) {
202
-			if ($this->configID !== '' && $this->configID !== null) {
203
-				$this->configuration->saveConfiguration();
204
-			}
205
-			$this->validateConfiguration();
206
-		}
207
-	}
208
-
209
-	/**
210
-	 * @param string $rule
211
-	 * @return array
212
-	 * @throws \RuntimeException
213
-	 */
214
-	public function resolveRule($rule) {
215
-		return $this->configuration->resolveRule($rule);
216
-	}
217
-
218
-	/**
219
-	 * sets whether the result of the configuration validation shall
220
-	 * be ignored when establishing the connection. Used by the Wizard
221
-	 * in early configuration state.
222
-	 * @param bool $state
223
-	 */
224
-	public function setIgnoreValidation($state) {
225
-		$this->ignoreValidation = (bool)$state;
226
-	}
227
-
228
-	/**
229
-	 * initializes the LDAP backend
230
-	 * @param bool $force read the config settings no matter what
231
-	 */
232
-	public function init($force = false) {
233
-		$this->readConfiguration($force);
234
-		$this->establishConnection();
235
-	}
236
-
237
-	/**
238
-	 * @return \LDAP\Connection The LDAP resource
239
-	 */
240
-	public function getConnectionResource(): \LDAP\Connection {
241
-		if (!$this->ldapConnectionRes) {
242
-			$this->init();
243
-		}
244
-		if (is_null($this->ldapConnectionRes)) {
245
-			$this->logger->error(
246
-				'No LDAP Connection to server ' . $this->configuration->ldapHost,
247
-				['app' => 'user_ldap']
248
-			);
249
-			throw new ServerNotAvailableException('Connection to LDAP server could not be established');
250
-		}
251
-		return $this->ldapConnectionRes;
252
-	}
253
-
254
-	/**
255
-	 * resets the connection resource
256
-	 */
257
-	public function resetConnectionResource(): void {
258
-		if (!is_null($this->ldapConnectionRes)) {
259
-			@$this->ldap->unbind($this->ldapConnectionRes);
260
-			$this->ldapConnectionRes = null;
261
-			$this->bindResult = [];
262
-		}
263
-	}
264
-
265
-	/**
266
-	 * @param string|null $key
267
-	 */
268
-	private function getCacheKey($key): string {
269
-		$prefix = 'LDAP-' . $this->configID . '-' . $this->configPrefix . '-';
270
-		if (is_null($key)) {
271
-			return $prefix;
272
-		}
273
-		return $prefix . hash('sha256', $key);
274
-	}
275
-
276
-	/**
277
-	 * @param string $key
278
-	 * @return mixed|null
279
-	 */
280
-	public function getFromCache($key) {
281
-		if (!$this->configured) {
282
-			$this->readConfiguration();
283
-		}
284
-		if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
285
-			return null;
286
-		}
287
-		$key = $this->getCacheKey($key);
288
-
289
-		return json_decode(base64_decode($this->cache->get($key) ?? ''), true);
290
-	}
291
-
292
-	public function getConfigPrefix(): string {
293
-		return $this->configPrefix;
294
-	}
295
-
296
-	/**
297
-	 * @param string $key
298
-	 * @param mixed $value
299
-	 */
300
-	public function writeToCache($key, $value, ?int $ttlOverride = null): void {
301
-		if (!$this->configured) {
302
-			$this->readConfiguration();
303
-		}
304
-		if (is_null($this->cache)
305
-			|| !$this->configuration->ldapCacheTTL
306
-			|| !$this->configuration->ldapConfigurationActive) {
307
-			return;
308
-		}
309
-		$key = $this->getCacheKey($key);
310
-		$value = base64_encode(json_encode($value));
311
-		$ttl = $ttlOverride ?? $this->configuration->ldapCacheTTL;
312
-		$this->cache->set($key, $value, $ttl);
313
-	}
314
-
315
-	public function clearCache() {
316
-		if (!is_null($this->cache)) {
317
-			$this->cache->clear($this->getCacheKey(null));
318
-		}
319
-	}
320
-
321
-	/**
322
-	 * Caches the general LDAP configuration.
323
-	 * @param bool $force optional. true, if the re-read should be forced. defaults
324
-	 *                    to false.
325
-	 */
326
-	private function readConfiguration(bool $force = false): void {
327
-		if ((!$this->configured || $force) && !is_null($this->configID)) {
328
-			$this->configuration->readConfiguration();
329
-			$this->configured = $this->validateConfiguration();
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * set LDAP configuration with values delivered by an array, not read from configuration
335
-	 * @param array $config array that holds the config parameters in an associated array
336
-	 * @param array &$setParameters optional; array where the set fields will be given to
337
-	 * @param bool $throw if true, throw ConfigurationIssueException with details instead of returning false
338
-	 * @return bool true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
339
-	 */
340
-	public function setConfiguration(array $config, ?array &$setParameters = null, bool $throw = false): bool {
341
-		if (is_null($setParameters)) {
342
-			$setParameters = [];
343
-		}
344
-		$this->doNotValidate = false;
345
-		$this->configuration->setConfiguration($config, $setParameters);
346
-		if (count($setParameters) > 0) {
347
-			$this->configured = $this->validateConfiguration($throw);
348
-		}
349
-
350
-
351
-		return $this->configured;
352
-	}
353
-
354
-	/**
355
-	 * saves the current Configuration in the database and empties the
356
-	 * cache
357
-	 * @return null
358
-	 */
359
-	public function saveConfiguration() {
360
-		$this->configuration->saveConfiguration();
361
-		$this->clearCache();
362
-	}
363
-
364
-	/**
365
-	 * get the current LDAP configuration
366
-	 * @return array
367
-	 */
368
-	public function getConfiguration() {
369
-		$this->readConfiguration();
370
-		$config = $this->configuration->getConfiguration();
371
-		$cta = $this->configuration->getConfigTranslationArray();
372
-		$result = [];
373
-		foreach ($cta as $dbkey => $configkey) {
374
-			switch ($configkey) {
375
-				case 'homeFolderNamingRule':
376
-					if (str_starts_with($config[$configkey], 'attr:')) {
377
-						$result[$dbkey] = substr($config[$configkey], 5);
378
-					} else {
379
-						$result[$dbkey] = '';
380
-					}
381
-					break;
382
-				case 'ldapBase':
383
-				case 'ldapBaseUsers':
384
-				case 'ldapBaseGroups':
385
-				case 'ldapAttributesForUserSearch':
386
-				case 'ldapAttributesForGroupSearch':
387
-					if (is_array($config[$configkey])) {
388
-						$result[$dbkey] = implode("\n", $config[$configkey]);
389
-						break;
390
-					} //else follows default
391
-					// no break
392
-				default:
393
-					$result[$dbkey] = $config[$configkey];
394
-			}
395
-		}
396
-		return $result;
397
-	}
398
-
399
-	private function doSoftValidation(): void {
400
-		//if User or Group Base are not set, take over Base DN setting
401
-		foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
402
-			$val = $this->configuration->$keyBase;
403
-			if (empty($val)) {
404
-				$this->configuration->$keyBase = $this->configuration->ldapBase;
405
-			}
406
-		}
407
-
408
-		foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
409
-			'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] as $expertSetting => $effectiveSetting) {
410
-			$uuidOverride = $this->configuration->$expertSetting;
411
-			if (!empty($uuidOverride)) {
412
-				$this->configuration->$effectiveSetting = $uuidOverride;
413
-			} else {
414
-				$uuidAttributes = Access::UUID_ATTRIBUTES;
415
-				array_unshift($uuidAttributes, 'auto');
416
-				if (!in_array($this->configuration->$effectiveSetting, $uuidAttributes)
417
-					&& !is_null($this->configID)) {
418
-					$this->configuration->$effectiveSetting = 'auto';
419
-					$this->configuration->saveConfiguration();
420
-					$this->logger->info(
421
-						'Illegal value for the ' . $effectiveSetting . ', reset to autodetect.',
422
-						['app' => 'user_ldap']
423
-					);
424
-				}
425
-			}
426
-		}
427
-
428
-		$backupPort = (int)$this->configuration->ldapBackupPort;
429
-		if ($backupPort <= 0) {
430
-			$this->configuration->ldapBackupPort = $this->configuration->ldapPort;
431
-		}
432
-
433
-		//make sure empty search attributes are saved as simple, empty array
434
-		$saKeys = ['ldapAttributesForUserSearch',
435
-			'ldapAttributesForGroupSearch'];
436
-		foreach ($saKeys as $key) {
437
-			$val = $this->configuration->$key;
438
-			if (is_array($val) && count($val) === 1 && empty($val[0])) {
439
-				$this->configuration->$key = [];
440
-			}
441
-		}
442
-
443
-		if ((stripos((string)$this->configuration->ldapHost, 'ldaps://') === 0)
444
-			&& $this->configuration->ldapTLS) {
445
-			$this->configuration->ldapTLS = (string)false;
446
-			$this->logger->info(
447
-				'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
448
-				['app' => 'user_ldap']
449
-			);
450
-		}
451
-	}
452
-
453
-	/**
454
-	 * @throws ConfigurationIssueException
455
-	 */
456
-	private function doCriticalValidation(): void {
457
-		//options that shall not be empty
458
-		$options = ['ldapHost', 'ldapUserDisplayName',
459
-			'ldapGroupDisplayName', 'ldapLoginFilter'];
460
-
461
-		//ldapPort should not be empty either unless ldapHost is pointing to a socket
462
-		if (!$this->configuration->usesLdapi()) {
463
-			$options[] = 'ldapPort';
464
-		}
465
-
466
-		foreach ($options as $key) {
467
-			$val = $this->configuration->$key;
468
-			if (empty($val)) {
469
-				switch ($key) {
470
-					case 'ldapHost':
471
-						$subj = 'LDAP Host';
472
-						break;
473
-					case 'ldapPort':
474
-						$subj = 'LDAP Port';
475
-						break;
476
-					case 'ldapUserDisplayName':
477
-						$subj = 'LDAP User Display Name';
478
-						break;
479
-					case 'ldapGroupDisplayName':
480
-						$subj = 'LDAP Group Display Name';
481
-						break;
482
-					case 'ldapLoginFilter':
483
-						$subj = 'LDAP Login Filter';
484
-						break;
485
-					default:
486
-						$subj = $key;
487
-						break;
488
-				}
489
-				throw new ConfigurationIssueException(
490
-					'No ' . $subj . ' given!',
491
-					$this->l10n->t('Mandatory field "%s" left empty', $subj),
492
-				);
493
-			}
494
-		}
495
-
496
-		//combinations
497
-		$agent = $this->configuration->ldapAgentName;
498
-		$pwd = $this->configuration->ldapAgentPassword;
499
-		if ($agent === '' && $pwd !== '') {
500
-			throw new ConfigurationIssueException(
501
-				'A password is given, but not an LDAP agent',
502
-				$this->l10n->t('A password is given, but not an LDAP agent'),
503
-			);
504
-		}
505
-		if ($agent !== '' && $pwd === '') {
506
-			throw new ConfigurationIssueException(
507
-				'No password is given for the user agent',
508
-				$this->l10n->t('No password is given for the user agent'),
509
-			);
510
-		}
511
-
512
-		$base = $this->configuration->ldapBase;
513
-		$baseUsers = $this->configuration->ldapBaseUsers;
514
-		$baseGroups = $this->configuration->ldapBaseGroups;
515
-
516
-		if (empty($base)) {
517
-			throw new ConfigurationIssueException(
518
-				'Not a single Base DN given',
519
-				$this->l10n->t('No LDAP base DN was given'),
520
-			);
521
-		}
522
-
523
-		if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
524
-			throw new ConfigurationIssueException(
525
-				'Login filter does not contain %uid placeholder.',
526
-				$this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
527
-			);
528
-		}
529
-	}
530
-
531
-	/**
532
-	 * Checks that all bases are subnodes of one of the root bases
533
-	 */
534
-	private function checkBasesAreValid(array $bases, array $rootBases): bool {
535
-		foreach ($bases as $base) {
536
-			$ok = false;
537
-			foreach ($rootBases as $rootBase) {
538
-				if (str_ends_with($base, $rootBase)) {
539
-					$ok = true;
540
-					break;
541
-				}
542
-			}
543
-			if (!$ok) {
544
-				return false;
545
-			}
546
-		}
547
-		return true;
548
-	}
549
-
550
-	/**
551
-	 * Validates the user specified configuration
552
-	 * @return bool true if configuration seems OK, false otherwise
553
-	 */
554
-	private function validateConfiguration(bool $throw = false): bool {
555
-		if ($this->doNotValidate) {
556
-			//don't do a validation if it is a new configuration with pure
557
-			//default values. Will be allowed on changes via __set or
558
-			//setConfiguration
559
-			return false;
560
-		}
561
-
562
-		// first step: "soft" checks: settings that are not really
563
-		// necessary, but advisable. If left empty, give an info message
564
-		$this->doSoftValidation();
565
-
566
-		//second step: critical checks. If left empty or filled wrong, mark as
567
-		//not configured and give a warning.
568
-		try {
569
-			$this->doCriticalValidation();
570
-			return true;
571
-		} catch (ConfigurationIssueException $e) {
572
-			if ($throw) {
573
-				throw $e;
574
-			}
575
-			$this->logger->warning(
576
-				'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
577
-				['exception' => $e]
578
-			);
579
-			return false;
580
-		}
581
-	}
582
-
583
-
584
-	/**
585
-	 * Connects and Binds to LDAP
586
-	 *
587
-	 * @throws ServerNotAvailableException
588
-	 */
589
-	private function establishConnection(): ?bool {
590
-		if (!$this->configuration->ldapConfigurationActive) {
591
-			return null;
592
-		}
593
-		static $phpLDAPinstalled = true;
594
-		if (!$phpLDAPinstalled) {
595
-			return false;
596
-		}
597
-		if (!$this->ignoreValidation && !$this->configured) {
598
-			$this->logger->warning(
599
-				'Configuration is invalid, cannot connect',
600
-				['app' => 'user_ldap']
601
-			);
602
-			return false;
603
-		}
604
-		if (!$this->ldapConnectionRes) {
605
-			if (!$this->ldap->areLDAPFunctionsAvailable()) {
606
-				$phpLDAPinstalled = false;
607
-				$this->logger->error(
608
-					'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
609
-					['app' => 'user_ldap']
610
-				);
611
-
612
-				return false;
613
-			}
614
-
615
-			$hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
616
-			$hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
617
-			$useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
618
-			$overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
619
-			$forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
620
-			$bindStatus = false;
621
-			if (!$forceBackupHost) {
622
-				try {
623
-					$host = $this->configuration->ldapHost ?? '';
624
-					$port = $this->configuration->ldapPort ?? '';
625
-					if ($useBackgroundHost) {
626
-						$host = $this->configuration->ldapBackgroundHost ?? '';
627
-						$port = $this->configuration->ldapBackgroundPort ?? '';
628
-					}
629
-					$this->doConnect($host, $port);
630
-					return $this->bind();
631
-				} catch (ServerNotAvailableException $e) {
632
-					if (!$hasBackupHost) {
633
-						throw $e;
634
-					}
635
-				}
636
-				$this->logger->warning(
637
-					'Main LDAP not reachable, connecting to backup: {msg}',
638
-					[
639
-						'app' => 'user_ldap',
640
-						'msg' => $e->getMessage(),
641
-						'exception' => $e,
642
-					]
643
-				);
644
-			}
645
-
646
-			// if LDAP server is not reachable, try the Backup (Replica!) Server
647
-			$this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
648
-			$this->bindResult = [];
649
-			$bindStatus = $this->bind();
650
-			$error = $this->ldap->isResource($this->ldapConnectionRes)
651
-				? $this->ldap->errno($this->ldapConnectionRes) : -1;
652
-			if ($bindStatus && $error === 0 && !$forceBackupHost) {
653
-				//when bind to backup server succeeded and failed to main server,
654
-				//skip contacting it for 15min
655
-				$this->writeToCache($overrideCacheKey, true, 60 * 15);
656
-			}
657
-
658
-			return $bindStatus;
659
-		}
660
-		return null;
661
-	}
662
-
663
-	/**
664
-	 * @param string $host
665
-	 * @param string $port
666
-	 * @throws \OC\ServerNotAvailableException
667
-	 */
668
-	private function doConnect($host, $port): bool {
669
-		if ($host === '') {
670
-			return false;
671
-		}
672
-
673
-		if ($this->configuration->turnOffCertCheck) {
674
-			if ($this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
675
-				$this->logger->debug(
676
-					'Turned off SSL certificate validation successfully.',
677
-					['app' => 'user_ldap']
678
-				);
679
-			} else {
680
-				$this->logger->warning(
681
-					'Could not turn off SSL certificate validation.',
682
-					['app' => 'user_ldap']
683
-				);
684
-			}
685
-		} else {
686
-			$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_DEMAND);
687
-		}
688
-
689
-		$this->ldapConnectionRes = $this->ldap->connect($host, $port) ?: null;
690
-
691
-		if ($this->ldapConnectionRes === null) {
692
-			throw new ServerNotAvailableException('Connection failed');
693
-		}
694
-
695
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
696
-			throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
697
-		}
698
-
699
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
700
-			throw new ServerNotAvailableException('Could not disable LDAP referrals.');
701
-		}
702
-
703
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
704
-			throw new ServerNotAvailableException('Could not set network timeout');
705
-		}
706
-
707
-		if ($this->configuration->ldapTLS) {
708
-			if (!$this->ldap->startTls($this->ldapConnectionRes)) {
709
-				throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
710
-			}
711
-		}
712
-
713
-		return true;
714
-	}
715
-
716
-	/**
717
-	 * Binds to LDAP
718
-	 */
719
-	public function bind() {
720
-		if (!$this->configuration->ldapConfigurationActive) {
721
-			return false;
722
-		}
723
-		$cr = $this->ldapConnectionRes;
724
-		if (!$this->ldap->isResource($cr)) {
725
-			$cr = $this->getConnectionResource();
726
-		}
727
-
728
-		if (
729
-			count($this->bindResult) !== 0
730
-			&& $this->bindResult['sum'] === md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword)
731
-		) {
732
-			// don't attempt to bind again with the same data as before
733
-			// bind might have been invoked via getConnectionResource(),
734
-			// but we need results specifically for e.g. user login
735
-			return $this->bindResult['result'];
736
-		}
737
-
738
-		$ldapLogin = @$this->ldap->bind($cr,
739
-			$this->configuration->ldapAgentName,
740
-			$this->configuration->ldapAgentPassword);
741
-
742
-		$this->bindResult = [
743
-			'sum' => md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword),
744
-			'result' => $ldapLogin,
745
-		];
746
-
747
-		if (!$ldapLogin) {
748
-			$errno = $this->ldap->errno($cr);
749
-
750
-			$this->logger->warning(
751
-				'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
752
-				['app' => 'user_ldap']
753
-			);
754
-
755
-			// Set to failure mode, if LDAP error code is not one of
756
-			// - LDAP_SUCCESS (0)
757
-			// - LDAP_INVALID_CREDENTIALS (49)
758
-			// - LDAP_INSUFFICIENT_ACCESS (50, spotted Apple Open Directory)
759
-			// - LDAP_UNWILLING_TO_PERFORM (53, spotted eDirectory)
760
-			if (!in_array($errno, [0, 49, 50, 53], true)) {
761
-				$this->ldapConnectionRes = null;
762
-			}
763
-
764
-			return false;
765
-		}
766
-		return true;
767
-	}
97
+    private ?\LDAP\Connection $ldapConnectionRes = null;
98
+    private bool $configured = false;
99
+
100
+    /**
101
+     * @var bool whether connection should be kept on __destruct
102
+     */
103
+    private bool $dontDestruct = false;
104
+
105
+    /**
106
+     * @var bool runtime flag that indicates whether supported primary groups are available
107
+     */
108
+    public $hasPrimaryGroups = true;
109
+
110
+    /**
111
+     * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
112
+     */
113
+    public $hasGidNumber = true;
114
+
115
+    /**
116
+     * @var ICache|null
117
+     */
118
+    protected $cache = null;
119
+
120
+    /** @var Configuration settings handler * */
121
+    protected $configuration;
122
+
123
+    /**
124
+     * @var bool
125
+     */
126
+    protected $doNotValidate = false;
127
+
128
+    /**
129
+     * @var bool
130
+     */
131
+    protected $ignoreValidation = false;
132
+
133
+    /**
134
+     * @var array{sum?: string, result?: bool}
135
+     */
136
+    protected $bindResult = [];
137
+
138
+    protected LoggerInterface $logger;
139
+    private IL10N $l10n;
140
+
141
+    /**
142
+     * Constructor
143
+     * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
144
+     * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
145
+     */
146
+    public function __construct(
147
+        ILDAPWrapper $ldap,
148
+        private string $configPrefix = '',
149
+        private ?string $configID = 'user_ldap',
150
+    ) {
151
+        parent::__construct($ldap);
152
+        $this->configuration = new Configuration($this->configPrefix, !is_null($this->configID));
153
+        $memcache = Server::get(ICacheFactory::class);
154
+        if ($memcache->isAvailable()) {
155
+            $this->cache = $memcache->createDistributed();
156
+        }
157
+        $helper = Server::get(Helper::class);
158
+        $this->doNotValidate = !in_array($this->configPrefix,
159
+            $helper->getServerConfigurationPrefixes());
160
+        $this->logger = Server::get(LoggerInterface::class);
161
+        $this->l10n = Util::getL10N('user_ldap');
162
+    }
163
+
164
+    public function __destruct() {
165
+        if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
166
+            @$this->ldap->unbind($this->ldapConnectionRes);
167
+            $this->bindResult = [];
168
+        }
169
+    }
170
+
171
+    /**
172
+     * defines behaviour when the instance is cloned
173
+     */
174
+    public function __clone() {
175
+        $this->configuration = new Configuration($this->configPrefix,
176
+            !is_null($this->configID));
177
+        if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
178
+            $this->bindResult = [];
179
+        }
180
+        $this->ldapConnectionRes = null;
181
+        $this->dontDestruct = true;
182
+    }
183
+
184
+    public function __get(string $name) {
185
+        if (!$this->configured) {
186
+            $this->readConfiguration();
187
+        }
188
+
189
+        return $this->configuration->$name;
190
+    }
191
+
192
+    /**
193
+     * @param string $name
194
+     * @param mixed $value
195
+     */
196
+    public function __set($name, $value) {
197
+        $this->doNotValidate = false;
198
+        $before = $this->configuration->$name;
199
+        $this->configuration->$name = $value;
200
+        $after = $this->configuration->$name;
201
+        if ($before !== $after) {
202
+            if ($this->configID !== '' && $this->configID !== null) {
203
+                $this->configuration->saveConfiguration();
204
+            }
205
+            $this->validateConfiguration();
206
+        }
207
+    }
208
+
209
+    /**
210
+     * @param string $rule
211
+     * @return array
212
+     * @throws \RuntimeException
213
+     */
214
+    public function resolveRule($rule) {
215
+        return $this->configuration->resolveRule($rule);
216
+    }
217
+
218
+    /**
219
+     * sets whether the result of the configuration validation shall
220
+     * be ignored when establishing the connection. Used by the Wizard
221
+     * in early configuration state.
222
+     * @param bool $state
223
+     */
224
+    public function setIgnoreValidation($state) {
225
+        $this->ignoreValidation = (bool)$state;
226
+    }
227
+
228
+    /**
229
+     * initializes the LDAP backend
230
+     * @param bool $force read the config settings no matter what
231
+     */
232
+    public function init($force = false) {
233
+        $this->readConfiguration($force);
234
+        $this->establishConnection();
235
+    }
236
+
237
+    /**
238
+     * @return \LDAP\Connection The LDAP resource
239
+     */
240
+    public function getConnectionResource(): \LDAP\Connection {
241
+        if (!$this->ldapConnectionRes) {
242
+            $this->init();
243
+        }
244
+        if (is_null($this->ldapConnectionRes)) {
245
+            $this->logger->error(
246
+                'No LDAP Connection to server ' . $this->configuration->ldapHost,
247
+                ['app' => 'user_ldap']
248
+            );
249
+            throw new ServerNotAvailableException('Connection to LDAP server could not be established');
250
+        }
251
+        return $this->ldapConnectionRes;
252
+    }
253
+
254
+    /**
255
+     * resets the connection resource
256
+     */
257
+    public function resetConnectionResource(): void {
258
+        if (!is_null($this->ldapConnectionRes)) {
259
+            @$this->ldap->unbind($this->ldapConnectionRes);
260
+            $this->ldapConnectionRes = null;
261
+            $this->bindResult = [];
262
+        }
263
+    }
264
+
265
+    /**
266
+     * @param string|null $key
267
+     */
268
+    private function getCacheKey($key): string {
269
+        $prefix = 'LDAP-' . $this->configID . '-' . $this->configPrefix . '-';
270
+        if (is_null($key)) {
271
+            return $prefix;
272
+        }
273
+        return $prefix . hash('sha256', $key);
274
+    }
275
+
276
+    /**
277
+     * @param string $key
278
+     * @return mixed|null
279
+     */
280
+    public function getFromCache($key) {
281
+        if (!$this->configured) {
282
+            $this->readConfiguration();
283
+        }
284
+        if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
285
+            return null;
286
+        }
287
+        $key = $this->getCacheKey($key);
288
+
289
+        return json_decode(base64_decode($this->cache->get($key) ?? ''), true);
290
+    }
291
+
292
+    public function getConfigPrefix(): string {
293
+        return $this->configPrefix;
294
+    }
295
+
296
+    /**
297
+     * @param string $key
298
+     * @param mixed $value
299
+     */
300
+    public function writeToCache($key, $value, ?int $ttlOverride = null): void {
301
+        if (!$this->configured) {
302
+            $this->readConfiguration();
303
+        }
304
+        if (is_null($this->cache)
305
+            || !$this->configuration->ldapCacheTTL
306
+            || !$this->configuration->ldapConfigurationActive) {
307
+            return;
308
+        }
309
+        $key = $this->getCacheKey($key);
310
+        $value = base64_encode(json_encode($value));
311
+        $ttl = $ttlOverride ?? $this->configuration->ldapCacheTTL;
312
+        $this->cache->set($key, $value, $ttl);
313
+    }
314
+
315
+    public function clearCache() {
316
+        if (!is_null($this->cache)) {
317
+            $this->cache->clear($this->getCacheKey(null));
318
+        }
319
+    }
320
+
321
+    /**
322
+     * Caches the general LDAP configuration.
323
+     * @param bool $force optional. true, if the re-read should be forced. defaults
324
+     *                    to false.
325
+     */
326
+    private function readConfiguration(bool $force = false): void {
327
+        if ((!$this->configured || $force) && !is_null($this->configID)) {
328
+            $this->configuration->readConfiguration();
329
+            $this->configured = $this->validateConfiguration();
330
+        }
331
+    }
332
+
333
+    /**
334
+     * set LDAP configuration with values delivered by an array, not read from configuration
335
+     * @param array $config array that holds the config parameters in an associated array
336
+     * @param array &$setParameters optional; array where the set fields will be given to
337
+     * @param bool $throw if true, throw ConfigurationIssueException with details instead of returning false
338
+     * @return bool true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
339
+     */
340
+    public function setConfiguration(array $config, ?array &$setParameters = null, bool $throw = false): bool {
341
+        if (is_null($setParameters)) {
342
+            $setParameters = [];
343
+        }
344
+        $this->doNotValidate = false;
345
+        $this->configuration->setConfiguration($config, $setParameters);
346
+        if (count($setParameters) > 0) {
347
+            $this->configured = $this->validateConfiguration($throw);
348
+        }
349
+
350
+
351
+        return $this->configured;
352
+    }
353
+
354
+    /**
355
+     * saves the current Configuration in the database and empties the
356
+     * cache
357
+     * @return null
358
+     */
359
+    public function saveConfiguration() {
360
+        $this->configuration->saveConfiguration();
361
+        $this->clearCache();
362
+    }
363
+
364
+    /**
365
+     * get the current LDAP configuration
366
+     * @return array
367
+     */
368
+    public function getConfiguration() {
369
+        $this->readConfiguration();
370
+        $config = $this->configuration->getConfiguration();
371
+        $cta = $this->configuration->getConfigTranslationArray();
372
+        $result = [];
373
+        foreach ($cta as $dbkey => $configkey) {
374
+            switch ($configkey) {
375
+                case 'homeFolderNamingRule':
376
+                    if (str_starts_with($config[$configkey], 'attr:')) {
377
+                        $result[$dbkey] = substr($config[$configkey], 5);
378
+                    } else {
379
+                        $result[$dbkey] = '';
380
+                    }
381
+                    break;
382
+                case 'ldapBase':
383
+                case 'ldapBaseUsers':
384
+                case 'ldapBaseGroups':
385
+                case 'ldapAttributesForUserSearch':
386
+                case 'ldapAttributesForGroupSearch':
387
+                    if (is_array($config[$configkey])) {
388
+                        $result[$dbkey] = implode("\n", $config[$configkey]);
389
+                        break;
390
+                    } //else follows default
391
+                    // no break
392
+                default:
393
+                    $result[$dbkey] = $config[$configkey];
394
+            }
395
+        }
396
+        return $result;
397
+    }
398
+
399
+    private function doSoftValidation(): void {
400
+        //if User or Group Base are not set, take over Base DN setting
401
+        foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
402
+            $val = $this->configuration->$keyBase;
403
+            if (empty($val)) {
404
+                $this->configuration->$keyBase = $this->configuration->ldapBase;
405
+            }
406
+        }
407
+
408
+        foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
409
+            'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute'] as $expertSetting => $effectiveSetting) {
410
+            $uuidOverride = $this->configuration->$expertSetting;
411
+            if (!empty($uuidOverride)) {
412
+                $this->configuration->$effectiveSetting = $uuidOverride;
413
+            } else {
414
+                $uuidAttributes = Access::UUID_ATTRIBUTES;
415
+                array_unshift($uuidAttributes, 'auto');
416
+                if (!in_array($this->configuration->$effectiveSetting, $uuidAttributes)
417
+                    && !is_null($this->configID)) {
418
+                    $this->configuration->$effectiveSetting = 'auto';
419
+                    $this->configuration->saveConfiguration();
420
+                    $this->logger->info(
421
+                        'Illegal value for the ' . $effectiveSetting . ', reset to autodetect.',
422
+                        ['app' => 'user_ldap']
423
+                    );
424
+                }
425
+            }
426
+        }
427
+
428
+        $backupPort = (int)$this->configuration->ldapBackupPort;
429
+        if ($backupPort <= 0) {
430
+            $this->configuration->ldapBackupPort = $this->configuration->ldapPort;
431
+        }
432
+
433
+        //make sure empty search attributes are saved as simple, empty array
434
+        $saKeys = ['ldapAttributesForUserSearch',
435
+            'ldapAttributesForGroupSearch'];
436
+        foreach ($saKeys as $key) {
437
+            $val = $this->configuration->$key;
438
+            if (is_array($val) && count($val) === 1 && empty($val[0])) {
439
+                $this->configuration->$key = [];
440
+            }
441
+        }
442
+
443
+        if ((stripos((string)$this->configuration->ldapHost, 'ldaps://') === 0)
444
+            && $this->configuration->ldapTLS) {
445
+            $this->configuration->ldapTLS = (string)false;
446
+            $this->logger->info(
447
+                'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
448
+                ['app' => 'user_ldap']
449
+            );
450
+        }
451
+    }
452
+
453
+    /**
454
+     * @throws ConfigurationIssueException
455
+     */
456
+    private function doCriticalValidation(): void {
457
+        //options that shall not be empty
458
+        $options = ['ldapHost', 'ldapUserDisplayName',
459
+            'ldapGroupDisplayName', 'ldapLoginFilter'];
460
+
461
+        //ldapPort should not be empty either unless ldapHost is pointing to a socket
462
+        if (!$this->configuration->usesLdapi()) {
463
+            $options[] = 'ldapPort';
464
+        }
465
+
466
+        foreach ($options as $key) {
467
+            $val = $this->configuration->$key;
468
+            if (empty($val)) {
469
+                switch ($key) {
470
+                    case 'ldapHost':
471
+                        $subj = 'LDAP Host';
472
+                        break;
473
+                    case 'ldapPort':
474
+                        $subj = 'LDAP Port';
475
+                        break;
476
+                    case 'ldapUserDisplayName':
477
+                        $subj = 'LDAP User Display Name';
478
+                        break;
479
+                    case 'ldapGroupDisplayName':
480
+                        $subj = 'LDAP Group Display Name';
481
+                        break;
482
+                    case 'ldapLoginFilter':
483
+                        $subj = 'LDAP Login Filter';
484
+                        break;
485
+                    default:
486
+                        $subj = $key;
487
+                        break;
488
+                }
489
+                throw new ConfigurationIssueException(
490
+                    'No ' . $subj . ' given!',
491
+                    $this->l10n->t('Mandatory field "%s" left empty', $subj),
492
+                );
493
+            }
494
+        }
495
+
496
+        //combinations
497
+        $agent = $this->configuration->ldapAgentName;
498
+        $pwd = $this->configuration->ldapAgentPassword;
499
+        if ($agent === '' && $pwd !== '') {
500
+            throw new ConfigurationIssueException(
501
+                'A password is given, but not an LDAP agent',
502
+                $this->l10n->t('A password is given, but not an LDAP agent'),
503
+            );
504
+        }
505
+        if ($agent !== '' && $pwd === '') {
506
+            throw new ConfigurationIssueException(
507
+                'No password is given for the user agent',
508
+                $this->l10n->t('No password is given for the user agent'),
509
+            );
510
+        }
511
+
512
+        $base = $this->configuration->ldapBase;
513
+        $baseUsers = $this->configuration->ldapBaseUsers;
514
+        $baseGroups = $this->configuration->ldapBaseGroups;
515
+
516
+        if (empty($base)) {
517
+            throw new ConfigurationIssueException(
518
+                'Not a single Base DN given',
519
+                $this->l10n->t('No LDAP base DN was given'),
520
+            );
521
+        }
522
+
523
+        if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
524
+            throw new ConfigurationIssueException(
525
+                'Login filter does not contain %uid placeholder.',
526
+                $this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
527
+            );
528
+        }
529
+    }
530
+
531
+    /**
532
+     * Checks that all bases are subnodes of one of the root bases
533
+     */
534
+    private function checkBasesAreValid(array $bases, array $rootBases): bool {
535
+        foreach ($bases as $base) {
536
+            $ok = false;
537
+            foreach ($rootBases as $rootBase) {
538
+                if (str_ends_with($base, $rootBase)) {
539
+                    $ok = true;
540
+                    break;
541
+                }
542
+            }
543
+            if (!$ok) {
544
+                return false;
545
+            }
546
+        }
547
+        return true;
548
+    }
549
+
550
+    /**
551
+     * Validates the user specified configuration
552
+     * @return bool true if configuration seems OK, false otherwise
553
+     */
554
+    private function validateConfiguration(bool $throw = false): bool {
555
+        if ($this->doNotValidate) {
556
+            //don't do a validation if it is a new configuration with pure
557
+            //default values. Will be allowed on changes via __set or
558
+            //setConfiguration
559
+            return false;
560
+        }
561
+
562
+        // first step: "soft" checks: settings that are not really
563
+        // necessary, but advisable. If left empty, give an info message
564
+        $this->doSoftValidation();
565
+
566
+        //second step: critical checks. If left empty or filled wrong, mark as
567
+        //not configured and give a warning.
568
+        try {
569
+            $this->doCriticalValidation();
570
+            return true;
571
+        } catch (ConfigurationIssueException $e) {
572
+            if ($throw) {
573
+                throw $e;
574
+            }
575
+            $this->logger->warning(
576
+                'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
577
+                ['exception' => $e]
578
+            );
579
+            return false;
580
+        }
581
+    }
582
+
583
+
584
+    /**
585
+     * Connects and Binds to LDAP
586
+     *
587
+     * @throws ServerNotAvailableException
588
+     */
589
+    private function establishConnection(): ?bool {
590
+        if (!$this->configuration->ldapConfigurationActive) {
591
+            return null;
592
+        }
593
+        static $phpLDAPinstalled = true;
594
+        if (!$phpLDAPinstalled) {
595
+            return false;
596
+        }
597
+        if (!$this->ignoreValidation && !$this->configured) {
598
+            $this->logger->warning(
599
+                'Configuration is invalid, cannot connect',
600
+                ['app' => 'user_ldap']
601
+            );
602
+            return false;
603
+        }
604
+        if (!$this->ldapConnectionRes) {
605
+            if (!$this->ldap->areLDAPFunctionsAvailable()) {
606
+                $phpLDAPinstalled = false;
607
+                $this->logger->error(
608
+                    'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
609
+                    ['app' => 'user_ldap']
610
+                );
611
+
612
+                return false;
613
+            }
614
+
615
+            $hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
616
+            $hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
617
+            $useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
618
+            $overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
619
+            $forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
620
+            $bindStatus = false;
621
+            if (!$forceBackupHost) {
622
+                try {
623
+                    $host = $this->configuration->ldapHost ?? '';
624
+                    $port = $this->configuration->ldapPort ?? '';
625
+                    if ($useBackgroundHost) {
626
+                        $host = $this->configuration->ldapBackgroundHost ?? '';
627
+                        $port = $this->configuration->ldapBackgroundPort ?? '';
628
+                    }
629
+                    $this->doConnect($host, $port);
630
+                    return $this->bind();
631
+                } catch (ServerNotAvailableException $e) {
632
+                    if (!$hasBackupHost) {
633
+                        throw $e;
634
+                    }
635
+                }
636
+                $this->logger->warning(
637
+                    'Main LDAP not reachable, connecting to backup: {msg}',
638
+                    [
639
+                        'app' => 'user_ldap',
640
+                        'msg' => $e->getMessage(),
641
+                        'exception' => $e,
642
+                    ]
643
+                );
644
+            }
645
+
646
+            // if LDAP server is not reachable, try the Backup (Replica!) Server
647
+            $this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
648
+            $this->bindResult = [];
649
+            $bindStatus = $this->bind();
650
+            $error = $this->ldap->isResource($this->ldapConnectionRes)
651
+                ? $this->ldap->errno($this->ldapConnectionRes) : -1;
652
+            if ($bindStatus && $error === 0 && !$forceBackupHost) {
653
+                //when bind to backup server succeeded and failed to main server,
654
+                //skip contacting it for 15min
655
+                $this->writeToCache($overrideCacheKey, true, 60 * 15);
656
+            }
657
+
658
+            return $bindStatus;
659
+        }
660
+        return null;
661
+    }
662
+
663
+    /**
664
+     * @param string $host
665
+     * @param string $port
666
+     * @throws \OC\ServerNotAvailableException
667
+     */
668
+    private function doConnect($host, $port): bool {
669
+        if ($host === '') {
670
+            return false;
671
+        }
672
+
673
+        if ($this->configuration->turnOffCertCheck) {
674
+            if ($this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
675
+                $this->logger->debug(
676
+                    'Turned off SSL certificate validation successfully.',
677
+                    ['app' => 'user_ldap']
678
+                );
679
+            } else {
680
+                $this->logger->warning(
681
+                    'Could not turn off SSL certificate validation.',
682
+                    ['app' => 'user_ldap']
683
+                );
684
+            }
685
+        } else {
686
+            $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_DEMAND);
687
+        }
688
+
689
+        $this->ldapConnectionRes = $this->ldap->connect($host, $port) ?: null;
690
+
691
+        if ($this->ldapConnectionRes === null) {
692
+            throw new ServerNotAvailableException('Connection failed');
693
+        }
694
+
695
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
696
+            throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
697
+        }
698
+
699
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
700
+            throw new ServerNotAvailableException('Could not disable LDAP referrals.');
701
+        }
702
+
703
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
704
+            throw new ServerNotAvailableException('Could not set network timeout');
705
+        }
706
+
707
+        if ($this->configuration->ldapTLS) {
708
+            if (!$this->ldap->startTls($this->ldapConnectionRes)) {
709
+                throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
710
+            }
711
+        }
712
+
713
+        return true;
714
+    }
715
+
716
+    /**
717
+     * Binds to LDAP
718
+     */
719
+    public function bind() {
720
+        if (!$this->configuration->ldapConfigurationActive) {
721
+            return false;
722
+        }
723
+        $cr = $this->ldapConnectionRes;
724
+        if (!$this->ldap->isResource($cr)) {
725
+            $cr = $this->getConnectionResource();
726
+        }
727
+
728
+        if (
729
+            count($this->bindResult) !== 0
730
+            && $this->bindResult['sum'] === md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword)
731
+        ) {
732
+            // don't attempt to bind again with the same data as before
733
+            // bind might have been invoked via getConnectionResource(),
734
+            // but we need results specifically for e.g. user login
735
+            return $this->bindResult['result'];
736
+        }
737
+
738
+        $ldapLogin = @$this->ldap->bind($cr,
739
+            $this->configuration->ldapAgentName,
740
+            $this->configuration->ldapAgentPassword);
741
+
742
+        $this->bindResult = [
743
+            'sum' => md5($this->configuration->ldapAgentName . $this->configPrefix . $this->configuration->ldapAgentPassword),
744
+            'result' => $ldapLogin,
745
+        ];
746
+
747
+        if (!$ldapLogin) {
748
+            $errno = $this->ldap->errno($cr);
749
+
750
+            $this->logger->warning(
751
+                'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
752
+                ['app' => 'user_ldap']
753
+            );
754
+
755
+            // Set to failure mode, if LDAP error code is not one of
756
+            // - LDAP_SUCCESS (0)
757
+            // - LDAP_INVALID_CREDENTIALS (49)
758
+            // - LDAP_INSUFFICIENT_ACCESS (50, spotted Apple Open Directory)
759
+            // - LDAP_UNWILLING_TO_PERFORM (53, spotted eDirectory)
760
+            if (!in_array($errno, [0, 49, 50, 53], true)) {
761
+                $this->ldapConnectionRes = null;
762
+            }
763
+
764
+            return false;
765
+        }
766
+        return true;
767
+    }
768 768
 }
Please login to merge, or discard this patch.