Completed
Push — master ( 9c8b50...ae0aa1 )
by
unknown
41:54 queued 12:01
created
apps/user_ldap/lib/Connection.php 2 patches
Indentation   +685 added lines, -685 removed lines patch added patch discarded remove patch
@@ -94,689 +94,689 @@
 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 (!empty($baseUsers) && !$this->checkBasesAreValid($baseUsers, $base)) {
524
-			throw new ConfigurationIssueException(
525
-				'User base is not in root base',
526
-				$this->l10n->t('User base DN is not a subnode of global base DN'),
527
-			);
528
-		}
529
-
530
-		if (!empty($baseGroups) && !$this->checkBasesAreValid($baseGroups, $base)) {
531
-			throw new ConfigurationIssueException(
532
-				'Group base is not in root base',
533
-				$this->l10n->t('Group base DN is not a subnode of global base DN'),
534
-			);
535
-		}
536
-
537
-		if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
538
-			throw new ConfigurationIssueException(
539
-				'Login filter does not contain %uid placeholder.',
540
-				$this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
541
-			);
542
-		}
543
-	}
544
-
545
-	/**
546
-	 * Checks that all bases are subnodes of one of the root bases
547
-	 */
548
-	private function checkBasesAreValid(array $bases, array $rootBases): bool {
549
-		foreach ($bases as $base) {
550
-			$ok = false;
551
-			foreach ($rootBases as $rootBase) {
552
-				if (str_ends_with($base, $rootBase)) {
553
-					$ok = true;
554
-					break;
555
-				}
556
-			}
557
-			if (!$ok) {
558
-				return false;
559
-			}
560
-		}
561
-		return true;
562
-	}
563
-
564
-	/**
565
-	 * Validates the user specified configuration
566
-	 * @return bool true if configuration seems OK, false otherwise
567
-	 */
568
-	private function validateConfiguration(bool $throw = false): bool {
569
-		if ($this->doNotValidate) {
570
-			//don't do a validation if it is a new configuration with pure
571
-			//default values. Will be allowed on changes via __set or
572
-			//setConfiguration
573
-			return false;
574
-		}
575
-
576
-		// first step: "soft" checks: settings that are not really
577
-		// necessary, but advisable. If left empty, give an info message
578
-		$this->doSoftValidation();
579
-
580
-		//second step: critical checks. If left empty or filled wrong, mark as
581
-		//not configured and give a warning.
582
-		try {
583
-			$this->doCriticalValidation();
584
-			return true;
585
-		} catch (ConfigurationIssueException $e) {
586
-			if ($throw) {
587
-				throw $e;
588
-			}
589
-			$this->logger->warning(
590
-				'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
591
-				['exception' => $e]
592
-			);
593
-			return false;
594
-		}
595
-	}
596
-
597
-
598
-	/**
599
-	 * Connects and Binds to LDAP
600
-	 *
601
-	 * @throws ServerNotAvailableException
602
-	 */
603
-	private function establishConnection(): ?bool {
604
-		if (!$this->configuration->ldapConfigurationActive) {
605
-			return null;
606
-		}
607
-		static $phpLDAPinstalled = true;
608
-		if (!$phpLDAPinstalled) {
609
-			return false;
610
-		}
611
-		if (!$this->ignoreValidation && !$this->configured) {
612
-			$this->logger->warning(
613
-				'Configuration is invalid, cannot connect',
614
-				['app' => 'user_ldap']
615
-			);
616
-			return false;
617
-		}
618
-		if (!$this->ldapConnectionRes) {
619
-			if (!$this->ldap->areLDAPFunctionsAvailable()) {
620
-				$phpLDAPinstalled = false;
621
-				$this->logger->error(
622
-					'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
623
-					['app' => 'user_ldap']
624
-				);
625
-
626
-				return false;
627
-			}
628
-
629
-			$hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
630
-			$hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
631
-			$useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
632
-			$overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
633
-			$forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
634
-			$bindStatus = false;
635
-			if (!$forceBackupHost) {
636
-				try {
637
-					$host = $this->configuration->ldapHost ?? '';
638
-					$port = $this->configuration->ldapPort ?? '';
639
-					if ($useBackgroundHost) {
640
-						$host = $this->configuration->ldapBackgroundHost ?? '';
641
-						$port = $this->configuration->ldapBackgroundPort ?? '';
642
-					}
643
-					$this->doConnect($host, $port);
644
-					return $this->bind();
645
-				} catch (ServerNotAvailableException $e) {
646
-					if (!$hasBackupHost) {
647
-						throw $e;
648
-					}
649
-				}
650
-				$this->logger->warning(
651
-					'Main LDAP not reachable, connecting to backup: {msg}',
652
-					[
653
-						'app' => 'user_ldap',
654
-						'msg' => $e->getMessage(),
655
-						'exception' => $e,
656
-					]
657
-				);
658
-			}
659
-
660
-			// if LDAP server is not reachable, try the Backup (Replica!) Server
661
-			$this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
662
-			$this->bindResult = [];
663
-			$bindStatus = $this->bind();
664
-			$error = $this->ldap->isResource($this->ldapConnectionRes)
665
-				? $this->ldap->errno($this->ldapConnectionRes) : -1;
666
-			if ($bindStatus && $error === 0 && !$forceBackupHost) {
667
-				//when bind to backup server succeeded and failed to main server,
668
-				//skip contacting it for 15min
669
-				$this->writeToCache($overrideCacheKey, true, 60 * 15);
670
-			}
671
-
672
-			return $bindStatus;
673
-		}
674
-		return null;
675
-	}
676
-
677
-	/**
678
-	 * @param string $host
679
-	 * @param string $port
680
-	 * @throws \OC\ServerNotAvailableException
681
-	 */
682
-	private function doConnect($host, $port): bool {
683
-		if ($host === '') {
684
-			return false;
685
-		}
686
-
687
-		if ($this->configuration->turnOffCertCheck) {
688
-			if ($this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
689
-				$this->logger->debug(
690
-					'Turned off SSL certificate validation successfully.',
691
-					['app' => 'user_ldap']
692
-				);
693
-			} else {
694
-				$this->logger->warning(
695
-					'Could not turn off SSL certificate validation.',
696
-					['app' => 'user_ldap']
697
-				);
698
-			}
699
-		} else {
700
-			$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_DEMAND);
701
-		}
702
-
703
-		$this->ldapConnectionRes = $this->ldap->connect($host, $port) ?: null;
704
-
705
-		if ($this->ldapConnectionRes === null) {
706
-			throw new ServerNotAvailableException('Connection failed');
707
-		}
708
-
709
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
710
-			throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
711
-		}
712
-
713
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
714
-			throw new ServerNotAvailableException('Could not disable LDAP referrals.');
715
-		}
716
-
717
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
718
-			throw new ServerNotAvailableException('Could not set network timeout');
719
-		}
720
-
721
-		if ($this->configuration->ldapTLS) {
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
-	}
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 (!empty($baseUsers) && !$this->checkBasesAreValid($baseUsers, $base)) {
524
+            throw new ConfigurationIssueException(
525
+                'User base is not in root base',
526
+                $this->l10n->t('User base DN is not a subnode of global base DN'),
527
+            );
528
+        }
529
+
530
+        if (!empty($baseGroups) && !$this->checkBasesAreValid($baseGroups, $base)) {
531
+            throw new ConfigurationIssueException(
532
+                'Group base is not in root base',
533
+                $this->l10n->t('Group base DN is not a subnode of global base DN'),
534
+            );
535
+        }
536
+
537
+        if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
538
+            throw new ConfigurationIssueException(
539
+                'Login filter does not contain %uid placeholder.',
540
+                $this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
541
+            );
542
+        }
543
+    }
544
+
545
+    /**
546
+     * Checks that all bases are subnodes of one of the root bases
547
+     */
548
+    private function checkBasesAreValid(array $bases, array $rootBases): bool {
549
+        foreach ($bases as $base) {
550
+            $ok = false;
551
+            foreach ($rootBases as $rootBase) {
552
+                if (str_ends_with($base, $rootBase)) {
553
+                    $ok = true;
554
+                    break;
555
+                }
556
+            }
557
+            if (!$ok) {
558
+                return false;
559
+            }
560
+        }
561
+        return true;
562
+    }
563
+
564
+    /**
565
+     * Validates the user specified configuration
566
+     * @return bool true if configuration seems OK, false otherwise
567
+     */
568
+    private function validateConfiguration(bool $throw = false): bool {
569
+        if ($this->doNotValidate) {
570
+            //don't do a validation if it is a new configuration with pure
571
+            //default values. Will be allowed on changes via __set or
572
+            //setConfiguration
573
+            return false;
574
+        }
575
+
576
+        // first step: "soft" checks: settings that are not really
577
+        // necessary, but advisable. If left empty, give an info message
578
+        $this->doSoftValidation();
579
+
580
+        //second step: critical checks. If left empty or filled wrong, mark as
581
+        //not configured and give a warning.
582
+        try {
583
+            $this->doCriticalValidation();
584
+            return true;
585
+        } catch (ConfigurationIssueException $e) {
586
+            if ($throw) {
587
+                throw $e;
588
+            }
589
+            $this->logger->warning(
590
+                'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
591
+                ['exception' => $e]
592
+            );
593
+            return false;
594
+        }
595
+    }
596
+
597
+
598
+    /**
599
+     * Connects and Binds to LDAP
600
+     *
601
+     * @throws ServerNotAvailableException
602
+     */
603
+    private function establishConnection(): ?bool {
604
+        if (!$this->configuration->ldapConfigurationActive) {
605
+            return null;
606
+        }
607
+        static $phpLDAPinstalled = true;
608
+        if (!$phpLDAPinstalled) {
609
+            return false;
610
+        }
611
+        if (!$this->ignoreValidation && !$this->configured) {
612
+            $this->logger->warning(
613
+                'Configuration is invalid, cannot connect',
614
+                ['app' => 'user_ldap']
615
+            );
616
+            return false;
617
+        }
618
+        if (!$this->ldapConnectionRes) {
619
+            if (!$this->ldap->areLDAPFunctionsAvailable()) {
620
+                $phpLDAPinstalled = false;
621
+                $this->logger->error(
622
+                    'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
623
+                    ['app' => 'user_ldap']
624
+                );
625
+
626
+                return false;
627
+            }
628
+
629
+            $hasBackupHost = (trim($this->configuration->ldapBackupHost ?? '') !== '');
630
+            $hasBackgroundHost = (trim($this->configuration->ldapBackgroundHost ?? '') !== '');
631
+            $useBackgroundHost = (\OC::$CLI && $hasBackgroundHost);
632
+            $overrideCacheKey = ($useBackgroundHost ? 'overrideBackgroundServer' : 'overrideMainServer');
633
+            $forceBackupHost = ($this->configuration->ldapOverrideMainServer || $this->getFromCache($overrideCacheKey));
634
+            $bindStatus = false;
635
+            if (!$forceBackupHost) {
636
+                try {
637
+                    $host = $this->configuration->ldapHost ?? '';
638
+                    $port = $this->configuration->ldapPort ?? '';
639
+                    if ($useBackgroundHost) {
640
+                        $host = $this->configuration->ldapBackgroundHost ?? '';
641
+                        $port = $this->configuration->ldapBackgroundPort ?? '';
642
+                    }
643
+                    $this->doConnect($host, $port);
644
+                    return $this->bind();
645
+                } catch (ServerNotAvailableException $e) {
646
+                    if (!$hasBackupHost) {
647
+                        throw $e;
648
+                    }
649
+                }
650
+                $this->logger->warning(
651
+                    'Main LDAP not reachable, connecting to backup: {msg}',
652
+                    [
653
+                        'app' => 'user_ldap',
654
+                        'msg' => $e->getMessage(),
655
+                        'exception' => $e,
656
+                    ]
657
+                );
658
+            }
659
+
660
+            // if LDAP server is not reachable, try the Backup (Replica!) Server
661
+            $this->doConnect($this->configuration->ldapBackupHost ?? '', $this->configuration->ldapBackupPort ?? '');
662
+            $this->bindResult = [];
663
+            $bindStatus = $this->bind();
664
+            $error = $this->ldap->isResource($this->ldapConnectionRes)
665
+                ? $this->ldap->errno($this->ldapConnectionRes) : -1;
666
+            if ($bindStatus && $error === 0 && !$forceBackupHost) {
667
+                //when bind to backup server succeeded and failed to main server,
668
+                //skip contacting it for 15min
669
+                $this->writeToCache($overrideCacheKey, true, 60 * 15);
670
+            }
671
+
672
+            return $bindStatus;
673
+        }
674
+        return null;
675
+    }
676
+
677
+    /**
678
+     * @param string $host
679
+     * @param string $port
680
+     * @throws \OC\ServerNotAvailableException
681
+     */
682
+    private function doConnect($host, $port): bool {
683
+        if ($host === '') {
684
+            return false;
685
+        }
686
+
687
+        if ($this->configuration->turnOffCertCheck) {
688
+            if ($this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER)) {
689
+                $this->logger->debug(
690
+                    'Turned off SSL certificate validation successfully.',
691
+                    ['app' => 'user_ldap']
692
+                );
693
+            } else {
694
+                $this->logger->warning(
695
+                    'Could not turn off SSL certificate validation.',
696
+                    ['app' => 'user_ldap']
697
+                );
698
+            }
699
+        } else {
700
+            $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_DEMAND);
701
+        }
702
+
703
+        $this->ldapConnectionRes = $this->ldap->connect($host, $port) ?: null;
704
+
705
+        if ($this->ldapConnectionRes === null) {
706
+            throw new ServerNotAvailableException('Connection failed');
707
+        }
708
+
709
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
710
+            throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
711
+        }
712
+
713
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
714
+            throw new ServerNotAvailableException('Could not disable LDAP referrals.');
715
+        }
716
+
717
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_NETWORK_TIMEOUT, $this->configuration->ldapConnectionTimeout)) {
718
+            throw new ServerNotAvailableException('Could not set network timeout');
719
+        }
720
+
721
+        if ($this->configuration->ldapTLS) {
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
@@ -222,7 +222,7 @@  discard block
 block discarded – undo
222 222
 	 * @param bool $state
223 223
 	 */
224 224
 	public function setIgnoreValidation($state) {
225
-		$this->ignoreValidation = (bool)$state;
225
+		$this->ignoreValidation = (bool) $state;
226 226
 	}
227 227
 
228 228
 	/**
@@ -243,7 +243,7 @@  discard block
 block discarded – undo
243 243
 		}
244 244
 		if (is_null($this->ldapConnectionRes)) {
245 245
 			$this->logger->error(
246
-				'No LDAP Connection to server ' . $this->configuration->ldapHost,
246
+				'No LDAP Connection to server '.$this->configuration->ldapHost,
247 247
 				['app' => 'user_ldap']
248 248
 			);
249 249
 			throw new ServerNotAvailableException('Connection to LDAP server could not be established');
@@ -266,11 +266,11 @@  discard block
 block discarded – undo
266 266
 	 * @param string|null $key
267 267
 	 */
268 268
 	private function getCacheKey($key): string {
269
-		$prefix = 'LDAP-' . $this->configID . '-' . $this->configPrefix . '-';
269
+		$prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
270 270
 		if (is_null($key)) {
271 271
 			return $prefix;
272 272
 		}
273
-		return $prefix . hash('sha256', $key);
273
+		return $prefix.hash('sha256', $key);
274 274
 	}
275 275
 
276 276
 	/**
@@ -418,14 +418,14 @@  discard block
 block discarded – undo
418 418
 					$this->configuration->$effectiveSetting = 'auto';
419 419
 					$this->configuration->saveConfiguration();
420 420
 					$this->logger->info(
421
-						'Illegal value for the ' . $effectiveSetting . ', reset to autodetect.',
421
+						'Illegal value for the '.$effectiveSetting.', reset to autodetect.',
422 422
 						['app' => 'user_ldap']
423 423
 					);
424 424
 				}
425 425
 			}
426 426
 		}
427 427
 
428
-		$backupPort = (int)$this->configuration->ldapBackupPort;
428
+		$backupPort = (int) $this->configuration->ldapBackupPort;
429 429
 		if ($backupPort <= 0) {
430 430
 			$this->configuration->ldapBackupPort = $this->configuration->ldapPort;
431 431
 		}
@@ -440,9 +440,9 @@  discard block
 block discarded – undo
440 440
 			}
441 441
 		}
442 442
 
443
-		if ((stripos((string)$this->configuration->ldapHost, 'ldaps://') === 0)
443
+		if ((stripos((string) $this->configuration->ldapHost, 'ldaps://') === 0)
444 444
 			&& $this->configuration->ldapTLS) {
445
-			$this->configuration->ldapTLS = (string)false;
445
+			$this->configuration->ldapTLS = (string) false;
446 446
 			$this->logger->info(
447 447
 				'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
448 448
 				['app' => 'user_ldap']
@@ -487,7 +487,7 @@  discard block
 block discarded – undo
487 487
 						break;
488 488
 				}
489 489
 				throw new ConfigurationIssueException(
490
-					'No ' . $subj . ' given!',
490
+					'No '.$subj.' given!',
491 491
 					$this->l10n->t('Mandatory field "%s" left empty', $subj),
492 492
 				);
493 493
 			}
@@ -534,7 +534,7 @@  discard block
 block discarded – undo
534 534
 			);
535 535
 		}
536 536
 
537
-		if (mb_strpos((string)$this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
537
+		if (mb_strpos((string) $this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8') === false) {
538 538
 			throw new ConfigurationIssueException(
539 539
 				'Login filter does not contain %uid placeholder.',
540 540
 				$this->l10n->t('Login filter does not contain %s placeholder.', ['%uid']),
@@ -587,7 +587,7 @@  discard block
 block discarded – undo
587 587
 				throw $e;
588 588
 			}
589 589
 			$this->logger->warning(
590
-				'Configuration Error (prefix ' . $this->configPrefix . '): ' . $e->getMessage(),
590
+				'Configuration Error (prefix '.$this->configPrefix.'): '.$e->getMessage(),
591 591
 				['exception' => $e]
592 592
 			);
593 593
 			return false;
@@ -720,7 +720,7 @@  discard block
 block discarded – undo
720 720
 
721 721
 		if ($this->configuration->ldapTLS) {
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.
apps/user_ldap/lib/ILDAPWrapper.php 1 patch
Indentation   +179 added lines, -179 removed lines patch added patch discarded remove patch
@@ -8,183 +8,183 @@
 block discarded – undo
8 8
 namespace OCA\User_LDAP;
9 9
 
10 10
 interface ILDAPWrapper {
11
-	//LDAP functions in use
12
-
13
-	/**
14
-	 * Bind to LDAP directory
15
-	 * @param \LDAP\Connection $link LDAP link resource
16
-	 * @param string $dn an RDN to log in with
17
-	 * @param string $password the password
18
-	 * @return bool true on success, false otherwise
19
-	 *
20
-	 * with $dn and $password as null a anonymous bind is attempted.
21
-	 */
22
-	public function bind($link, $dn, $password);
23
-
24
-	/**
25
-	 * connect to an LDAP server
26
-	 * @param string $host The host to connect to
27
-	 * @param string $port The port to connect to
28
-	 * @return \LDAP\Connection|false a link resource on success, otherwise false
29
-	 */
30
-	public function connect($host, $port);
31
-
32
-	/**
33
-	 * Retrieve the LDAP pagination cookie
34
-	 * @param \LDAP\Connection $link LDAP link resource
35
-	 * @param \LDAP\Result $result LDAP result resource
36
-	 * @param string &$cookie structure sent by LDAP server
37
-	 * @return bool true on success, false otherwise
38
-	 *
39
-	 * Corresponds to ldap_control_paged_result_response
40
-	 */
41
-	public function controlPagedResultResponse($link, $result, &$cookie);
42
-
43
-	/**
44
-	 * Count the number of entries in a search
45
-	 * @param \LDAP\Connection $link LDAP link resource
46
-	 * @param \LDAP\Result $result LDAP result resource
47
-	 * @return int|false number of results on success, false otherwise
48
-	 */
49
-	public function countEntries($link, $result);
50
-
51
-	/**
52
-	 * Return the LDAP error number of the last LDAP command
53
-	 * @param \LDAP\Connection $link LDAP link resource
54
-	 * @return int error code
55
-	 */
56
-	public function errno($link);
57
-
58
-	/**
59
-	 * Return the LDAP error message of the last LDAP command
60
-	 * @param \LDAP\Connection $link LDAP link resource
61
-	 * @return string error message
62
-	 */
63
-	public function error($link);
64
-
65
-	/**
66
-	 * Splits DN into its component parts
67
-	 * @param string $dn
68
-	 * @param int @withAttrib
69
-	 * @return array|false
70
-	 * @link https://www.php.net/manual/en/function.ldap-explode-dn.php
71
-	 */
72
-	public function explodeDN($dn, $withAttrib);
73
-
74
-	/**
75
-	 * Return first result id
76
-	 * @param \LDAP\Connection $link LDAP link resource
77
-	 * @param \LDAP\Result $result LDAP result resource
78
-	 * @return \LDAP\ResultEntry an LDAP entry resource
79
-	 * */
80
-	public function firstEntry($link, $result);
81
-
82
-	/**
83
-	 * Get attributes from a search result entry
84
-	 * @param \LDAP\Connection $link LDAP link resource
85
-	 * @param \LDAP\ResultEntry $result LDAP result resource
86
-	 * @return array|false containing the results, false on error
87
-	 * */
88
-	public function getAttributes($link, $result);
89
-
90
-	/**
91
-	 * Get the DN of a result entry
92
-	 * @param \LDAP\Connection $link LDAP link resource
93
-	 * @param \LDAP\ResultEntry $result LDAP result resource
94
-	 * @return string|false containing the DN, false on error
95
-	 */
96
-	public function getDN($link, $result);
97
-
98
-	/**
99
-	 * Get all result entries
100
-	 * @param \LDAP\Connection $link LDAP link resource
101
-	 * @param \LDAP\Result $result LDAP result resource
102
-	 * @return array|false containing the results, false on error
103
-	 */
104
-	public function getEntries($link, $result);
105
-
106
-	/**
107
-	 * Return next result id
108
-	 * @param \LDAP\Connection $link LDAP link resource
109
-	 * @param \LDAP\ResultEntry $result LDAP result resource
110
-	 * @return \LDAP\ResultEntry an LDAP entry resource
111
-	 * */
112
-	public function nextEntry($link, $result);
113
-
114
-	/**
115
-	 * Read an entry
116
-	 * @param \LDAP\Connection $link LDAP link resource
117
-	 * @param string $baseDN The DN of the entry to read from
118
-	 * @param string $filter An LDAP filter
119
-	 * @param array $attr array of the attributes to read
120
-	 * @return \LDAP\Result an LDAP search result resource
121
-	 */
122
-	public function read($link, $baseDN, $filter, $attr);
123
-
124
-	/**
125
-	 * Search LDAP tree
126
-	 * @param \LDAP\Connection $link LDAP link resource
127
-	 * @param string $baseDN The DN of the entry to read from
128
-	 * @param string $filter An LDAP filter
129
-	 * @param array $attr array of the attributes to read
130
-	 * @param int $attrsOnly optional, 1 if only attribute types shall be returned
131
-	 * @param int $limit optional, limits the result entries
132
-	 * @return \LDAP\Result|false an LDAP search result resource, false on error
133
-	 */
134
-	public function search($link, string $baseDN, string $filter, array $attr, int $attrsOnly = 0, int $limit = 0, int $pageSize = 0, string $cookie = '');
135
-
136
-	/**
137
-	 * Replace the value of a userPassword by $password
138
-	 * @param \LDAP\Connection $link LDAP link resource
139
-	 * @param string $userDN the DN of the user whose password is to be replaced
140
-	 * @param string $password the new value for the userPassword
141
-	 * @return bool true on success, false otherwise
142
-	 */
143
-	public function modReplace($link, $userDN, $password);
144
-
145
-	/**
146
-	 * Performs a PASSWD extended operation.
147
-	 * @param \LDAP\Connection $link LDAP link resource
148
-	 * @return bool|string The generated password if new_password is empty or omitted. Otherwise true on success and false on failure.
149
-	 */
150
-	public function exopPasswd($link, string $userDN, string $oldPassword, string $password);
151
-
152
-	/**
153
-	 * Sets the value of the specified option to be $value
154
-	 * @param ?\LDAP\Connection $link LDAP link resource
155
-	 * @param int $option a defined LDAP Server option
156
-	 * @param mixed $value the new value for the option
157
-	 * @return bool true on success, false otherwise
158
-	 */
159
-	public function setOption($link, $option, $value);
160
-
161
-	/**
162
-	 * establish Start TLS
163
-	 * @param \LDAP\Connection $link LDAP link resource
164
-	 * @return bool true on success, false otherwise
165
-	 */
166
-	public function startTls($link);
167
-
168
-	/**
169
-	 * Unbind from LDAP directory
170
-	 * @param \LDAP\Connection $link LDAP link resource
171
-	 * @return bool true on success, false otherwise
172
-	 */
173
-	public function unbind($link);
174
-
175
-	//additional required methods in Nextcloud
176
-
177
-	/**
178
-	 * Checks whether the server supports LDAP
179
-	 * @return bool true if it the case, false otherwise
180
-	 * */
181
-	public function areLDAPFunctionsAvailable();
182
-
183
-	/**
184
-	 * Checks whether the submitted parameter is a resource
185
-	 * @param mixed $resource the resource variable to check
186
-	 * @psalm-assert-if-true object $resource
187
-	 * @return bool true if it is a resource or LDAP object, false otherwise
188
-	 */
189
-	public function isResource($resource);
11
+    //LDAP functions in use
12
+
13
+    /**
14
+     * Bind to LDAP directory
15
+     * @param \LDAP\Connection $link LDAP link resource
16
+     * @param string $dn an RDN to log in with
17
+     * @param string $password the password
18
+     * @return bool true on success, false otherwise
19
+     *
20
+     * with $dn and $password as null a anonymous bind is attempted.
21
+     */
22
+    public function bind($link, $dn, $password);
23
+
24
+    /**
25
+     * connect to an LDAP server
26
+     * @param string $host The host to connect to
27
+     * @param string $port The port to connect to
28
+     * @return \LDAP\Connection|false a link resource on success, otherwise false
29
+     */
30
+    public function connect($host, $port);
31
+
32
+    /**
33
+     * Retrieve the LDAP pagination cookie
34
+     * @param \LDAP\Connection $link LDAP link resource
35
+     * @param \LDAP\Result $result LDAP result resource
36
+     * @param string &$cookie structure sent by LDAP server
37
+     * @return bool true on success, false otherwise
38
+     *
39
+     * Corresponds to ldap_control_paged_result_response
40
+     */
41
+    public function controlPagedResultResponse($link, $result, &$cookie);
42
+
43
+    /**
44
+     * Count the number of entries in a search
45
+     * @param \LDAP\Connection $link LDAP link resource
46
+     * @param \LDAP\Result $result LDAP result resource
47
+     * @return int|false number of results on success, false otherwise
48
+     */
49
+    public function countEntries($link, $result);
50
+
51
+    /**
52
+     * Return the LDAP error number of the last LDAP command
53
+     * @param \LDAP\Connection $link LDAP link resource
54
+     * @return int error code
55
+     */
56
+    public function errno($link);
57
+
58
+    /**
59
+     * Return the LDAP error message of the last LDAP command
60
+     * @param \LDAP\Connection $link LDAP link resource
61
+     * @return string error message
62
+     */
63
+    public function error($link);
64
+
65
+    /**
66
+     * Splits DN into its component parts
67
+     * @param string $dn
68
+     * @param int @withAttrib
69
+     * @return array|false
70
+     * @link https://www.php.net/manual/en/function.ldap-explode-dn.php
71
+     */
72
+    public function explodeDN($dn, $withAttrib);
73
+
74
+    /**
75
+     * Return first result id
76
+     * @param \LDAP\Connection $link LDAP link resource
77
+     * @param \LDAP\Result $result LDAP result resource
78
+     * @return \LDAP\ResultEntry an LDAP entry resource
79
+     * */
80
+    public function firstEntry($link, $result);
81
+
82
+    /**
83
+     * Get attributes from a search result entry
84
+     * @param \LDAP\Connection $link LDAP link resource
85
+     * @param \LDAP\ResultEntry $result LDAP result resource
86
+     * @return array|false containing the results, false on error
87
+     * */
88
+    public function getAttributes($link, $result);
89
+
90
+    /**
91
+     * Get the DN of a result entry
92
+     * @param \LDAP\Connection $link LDAP link resource
93
+     * @param \LDAP\ResultEntry $result LDAP result resource
94
+     * @return string|false containing the DN, false on error
95
+     */
96
+    public function getDN($link, $result);
97
+
98
+    /**
99
+     * Get all result entries
100
+     * @param \LDAP\Connection $link LDAP link resource
101
+     * @param \LDAP\Result $result LDAP result resource
102
+     * @return array|false containing the results, false on error
103
+     */
104
+    public function getEntries($link, $result);
105
+
106
+    /**
107
+     * Return next result id
108
+     * @param \LDAP\Connection $link LDAP link resource
109
+     * @param \LDAP\ResultEntry $result LDAP result resource
110
+     * @return \LDAP\ResultEntry an LDAP entry resource
111
+     * */
112
+    public function nextEntry($link, $result);
113
+
114
+    /**
115
+     * Read an entry
116
+     * @param \LDAP\Connection $link LDAP link resource
117
+     * @param string $baseDN The DN of the entry to read from
118
+     * @param string $filter An LDAP filter
119
+     * @param array $attr array of the attributes to read
120
+     * @return \LDAP\Result an LDAP search result resource
121
+     */
122
+    public function read($link, $baseDN, $filter, $attr);
123
+
124
+    /**
125
+     * Search LDAP tree
126
+     * @param \LDAP\Connection $link LDAP link resource
127
+     * @param string $baseDN The DN of the entry to read from
128
+     * @param string $filter An LDAP filter
129
+     * @param array $attr array of the attributes to read
130
+     * @param int $attrsOnly optional, 1 if only attribute types shall be returned
131
+     * @param int $limit optional, limits the result entries
132
+     * @return \LDAP\Result|false an LDAP search result resource, false on error
133
+     */
134
+    public function search($link, string $baseDN, string $filter, array $attr, int $attrsOnly = 0, int $limit = 0, int $pageSize = 0, string $cookie = '');
135
+
136
+    /**
137
+     * Replace the value of a userPassword by $password
138
+     * @param \LDAP\Connection $link LDAP link resource
139
+     * @param string $userDN the DN of the user whose password is to be replaced
140
+     * @param string $password the new value for the userPassword
141
+     * @return bool true on success, false otherwise
142
+     */
143
+    public function modReplace($link, $userDN, $password);
144
+
145
+    /**
146
+     * Performs a PASSWD extended operation.
147
+     * @param \LDAP\Connection $link LDAP link resource
148
+     * @return bool|string The generated password if new_password is empty or omitted. Otherwise true on success and false on failure.
149
+     */
150
+    public function exopPasswd($link, string $userDN, string $oldPassword, string $password);
151
+
152
+    /**
153
+     * Sets the value of the specified option to be $value
154
+     * @param ?\LDAP\Connection $link LDAP link resource
155
+     * @param int $option a defined LDAP Server option
156
+     * @param mixed $value the new value for the option
157
+     * @return bool true on success, false otherwise
158
+     */
159
+    public function setOption($link, $option, $value);
160
+
161
+    /**
162
+     * establish Start TLS
163
+     * @param \LDAP\Connection $link LDAP link resource
164
+     * @return bool true on success, false otherwise
165
+     */
166
+    public function startTls($link);
167
+
168
+    /**
169
+     * Unbind from LDAP directory
170
+     * @param \LDAP\Connection $link LDAP link resource
171
+     * @return bool true on success, false otherwise
172
+     */
173
+    public function unbind($link);
174
+
175
+    //additional required methods in Nextcloud
176
+
177
+    /**
178
+     * Checks whether the server supports LDAP
179
+     * @return bool true if it the case, false otherwise
180
+     * */
181
+    public function areLDAPFunctionsAvailable();
182
+
183
+    /**
184
+     * Checks whether the submitted parameter is a resource
185
+     * @param mixed $resource the resource variable to check
186
+     * @psalm-assert-if-true object $resource
187
+     * @return bool true if it is a resource or LDAP object, false otherwise
188
+     */
189
+    public function isResource($resource);
190 190
 }
Please login to merge, or discard this patch.