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