Passed
Push — master ( 8355f9...6d2471 )
by Blizzz
12:04 queued 10s
created
apps/user_ldap/lib/Connection.php 1 patch
Indentation   +611 added lines, -611 removed lines patch added patch discarded remove patch
@@ -75,615 +75,615 @@
 block discarded – undo
75 75
  * @property string ldapMatchingRuleInChainState
76 76
  */
77 77
 class Connection extends LDAPUtility {
78
-	private $ldapConnectionRes = null;
79
-	private $configPrefix;
80
-	private $configID;
81
-	private $configured = false;
82
-	//whether connection should be kept on __destruct
83
-	private $dontDestruct = false;
84
-
85
-	/**
86
-	 * @var bool runtime flag that indicates whether supported primary groups are available
87
-	 */
88
-	public $hasPrimaryGroups = true;
89
-
90
-	/**
91
-	 * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
92
-	 */
93
-	public $hasGidNumber = true;
94
-
95
-	//cache handler
96
-	protected $cache;
97
-
98
-	/** @var Configuration settings handler **/
99
-	protected $configuration;
100
-
101
-	protected $doNotValidate = false;
102
-
103
-	protected $ignoreValidation = false;
104
-
105
-	protected $bindResult = [];
106
-
107
-	/**
108
-	 * Constructor
109
-	 * @param ILDAPWrapper $ldap
110
-	 * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
111
-	 * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
112
-	 */
113
-	public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') {
114
-		parent::__construct($ldap);
115
-		$this->configPrefix = $configPrefix;
116
-		$this->configID = $configID;
117
-		$this->configuration = new Configuration($configPrefix,
118
-												 !is_null($configID));
119
-		$memcache = \OC::$server->getMemCacheFactory();
120
-		if ($memcache->isAvailable()) {
121
-			$this->cache = $memcache->createDistributed();
122
-		}
123
-		$helper = new Helper(\OC::$server->getConfig());
124
-		$this->doNotValidate = !in_array($this->configPrefix,
125
-			$helper->getServerConfigurationPrefixes());
126
-	}
127
-
128
-	public function __destruct() {
129
-		if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
130
-			@$this->ldap->unbind($this->ldapConnectionRes);
131
-			$this->bindResult = [];
132
-		}
133
-	}
134
-
135
-	/**
136
-	 * defines behaviour when the instance is cloned
137
-	 */
138
-	public function __clone() {
139
-		$this->configuration = new Configuration($this->configPrefix,
140
-												 !is_null($this->configID));
141
-		if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
142
-			$this->bindResult = [];
143
-		}
144
-		$this->ldapConnectionRes = null;
145
-		$this->dontDestruct = true;
146
-	}
147
-
148
-	public function __get(string $name) {
149
-		if (!$this->configured) {
150
-			$this->readConfiguration();
151
-		}
152
-
153
-		return $this->configuration->$name;
154
-	}
155
-
156
-	/**
157
-	 * @param string $name
158
-	 * @param mixed $value
159
-	 */
160
-	public function __set($name, $value) {
161
-		$this->doNotValidate = false;
162
-		$before = $this->configuration->$name;
163
-		$this->configuration->$name = $value;
164
-		$after = $this->configuration->$name;
165
-		if ($before !== $after) {
166
-			if ($this->configID !== '' && $this->configID !== null) {
167
-				$this->configuration->saveConfiguration();
168
-			}
169
-			$this->validateConfiguration();
170
-		}
171
-	}
172
-
173
-	/**
174
-	 * @param string $rule
175
-	 * @return array
176
-	 * @throws \RuntimeException
177
-	 */
178
-	public function resolveRule($rule) {
179
-		return $this->configuration->resolveRule($rule);
180
-	}
181
-
182
-	/**
183
-	 * sets whether the result of the configuration validation shall
184
-	 * be ignored when establishing the connection. Used by the Wizard
185
-	 * in early configuration state.
186
-	 * @param bool $state
187
-	 */
188
-	public function setIgnoreValidation($state) {
189
-		$this->ignoreValidation = (bool)$state;
190
-	}
191
-
192
-	/**
193
-	 * initializes the LDAP backend
194
-	 * @param bool $force read the config settings no matter what
195
-	 */
196
-	public function init($force = false) {
197
-		$this->readConfiguration($force);
198
-		$this->establishConnection();
199
-	}
200
-
201
-	/**
202
-	 * Returns the LDAP handler
203
-	 */
204
-	public function getConnectionResource() {
205
-		if (!$this->ldapConnectionRes) {
206
-			$this->init();
207
-		} elseif (!$this->ldap->isResource($this->ldapConnectionRes)) {
208
-			$this->ldapConnectionRes = null;
209
-			$this->establishConnection();
210
-		}
211
-		if (is_null($this->ldapConnectionRes)) {
212
-			\OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, ILogger::ERROR);
213
-			throw new ServerNotAvailableException('Connection to LDAP server could not be established');
214
-		}
215
-		return $this->ldapConnectionRes;
216
-	}
217
-
218
-	/**
219
-	 * resets the connection resource
220
-	 */
221
-	public function resetConnectionResource() {
222
-		if (!is_null($this->ldapConnectionRes)) {
223
-			@$this->ldap->unbind($this->ldapConnectionRes);
224
-			$this->ldapConnectionRes = null;
225
-			$this->bindResult = [];
226
-		}
227
-	}
228
-
229
-	/**
230
-	 * @param string|null $key
231
-	 * @return string
232
-	 */
233
-	private function getCacheKey($key) {
234
-		$prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
235
-		if (is_null($key)) {
236
-			return $prefix;
237
-		}
238
-		return $prefix.hash('sha256', $key);
239
-	}
240
-
241
-	/**
242
-	 * @param string $key
243
-	 * @return mixed|null
244
-	 */
245
-	public function getFromCache($key) {
246
-		if (!$this->configured) {
247
-			$this->readConfiguration();
248
-		}
249
-		if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
250
-			return null;
251
-		}
252
-		$key = $this->getCacheKey($key);
253
-
254
-		return json_decode(base64_decode($this->cache->get($key)), true);
255
-	}
256
-
257
-	/**
258
-	 * @param string $key
259
-	 * @param mixed $value
260
-	 *
261
-	 * @return string
262
-	 */
263
-	public function writeToCache($key, $value) {
264
-		if (!$this->configured) {
265
-			$this->readConfiguration();
266
-		}
267
-		if (is_null($this->cache)
268
-			|| !$this->configuration->ldapCacheTTL
269
-			|| !$this->configuration->ldapConfigurationActive) {
270
-			return null;
271
-		}
272
-		$key = $this->getCacheKey($key);
273
-		$value = base64_encode(json_encode($value));
274
-		$this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
275
-	}
276
-
277
-	public function clearCache() {
278
-		if (!is_null($this->cache)) {
279
-			$this->cache->clear($this->getCacheKey(null));
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * Caches the general LDAP configuration.
285
-	 * @param bool $force optional. true, if the re-read should be forced. defaults
286
-	 * to false.
287
-	 * @return null
288
-	 */
289
-	private function readConfiguration($force = false) {
290
-		if ((!$this->configured || $force) && !is_null($this->configID)) {
291
-			$this->configuration->readConfiguration();
292
-			$this->configured = $this->validateConfiguration();
293
-		}
294
-	}
295
-
296
-	/**
297
-	 * set LDAP configuration with values delivered by an array, not read from configuration
298
-	 * @param array $config array that holds the config parameters in an associated array
299
-	 * @param array &$setParameters optional; array where the set fields will be given to
300
-	 * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
301
-	 */
302
-	public function setConfiguration($config, &$setParameters = null) {
303
-		if (is_null($setParameters)) {
304
-			$setParameters = [];
305
-		}
306
-		$this->doNotValidate = false;
307
-		$this->configuration->setConfiguration($config, $setParameters);
308
-		if (count($setParameters) > 0) {
309
-			$this->configured = $this->validateConfiguration();
310
-		}
311
-
312
-
313
-		return $this->configured;
314
-	}
315
-
316
-	/**
317
-	 * saves the current Configuration in the database and empties the
318
-	 * cache
319
-	 * @return null
320
-	 */
321
-	public function saveConfiguration() {
322
-		$this->configuration->saveConfiguration();
323
-		$this->clearCache();
324
-	}
325
-
326
-	/**
327
-	 * get the current LDAP configuration
328
-	 * @return array
329
-	 */
330
-	public function getConfiguration() {
331
-		$this->readConfiguration();
332
-		$config = $this->configuration->getConfiguration();
333
-		$cta = $this->configuration->getConfigTranslationArray();
334
-		$result = [];
335
-		foreach ($cta as $dbkey => $configkey) {
336
-			switch ($configkey) {
337
-				case 'homeFolderNamingRule':
338
-					if (strpos($config[$configkey], 'attr:') === 0) {
339
-						$result[$dbkey] = substr($config[$configkey], 5);
340
-					} else {
341
-						$result[$dbkey] = '';
342
-					}
343
-					break;
344
-				case 'ldapBase':
345
-				case 'ldapBaseUsers':
346
-				case 'ldapBaseGroups':
347
-				case 'ldapAttributesForUserSearch':
348
-				case 'ldapAttributesForGroupSearch':
349
-					if (is_array($config[$configkey])) {
350
-						$result[$dbkey] = implode("\n", $config[$configkey]);
351
-						break;
352
-					} //else follows default
353
-					// no break
354
-				default:
355
-					$result[$dbkey] = $config[$configkey];
356
-			}
357
-		}
358
-		return $result;
359
-	}
360
-
361
-	private function doSoftValidation() {
362
-		//if User or Group Base are not set, take over Base DN setting
363
-		foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
364
-			$val = $this->configuration->$keyBase;
365
-			if (empty($val)) {
366
-				$this->configuration->$keyBase = $this->configuration->ldapBase;
367
-			}
368
-		}
369
-
370
-		foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
371
-			'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute']
372
-				as $expertSetting => $effectiveSetting) {
373
-			$uuidOverride = $this->configuration->$expertSetting;
374
-			if (!empty($uuidOverride)) {
375
-				$this->configuration->$effectiveSetting = $uuidOverride;
376
-			} else {
377
-				$uuidAttributes = Access::UUID_ATTRIBUTES;
378
-				array_unshift($uuidAttributes, 'auto');
379
-				if (!in_array($this->configuration->$effectiveSetting,
380
-							$uuidAttributes)
381
-					&& (!is_null($this->configID))) {
382
-					$this->configuration->$effectiveSetting = 'auto';
383
-					$this->configuration->saveConfiguration();
384
-					\OCP\Util::writeLog('user_ldap',
385
-										'Illegal value for the '.
386
-										$effectiveSetting.', '.'reset to '.
387
-										'autodetect.', ILogger::INFO);
388
-				}
389
-			}
390
-		}
391
-
392
-		$backupPort = (int)$this->configuration->ldapBackupPort;
393
-		if ($backupPort <= 0) {
394
-			$this->configuration->backupPort = $this->configuration->ldapPort;
395
-		}
396
-
397
-		//make sure empty search attributes are saved as simple, empty array
398
-		$saKeys = ['ldapAttributesForUserSearch',
399
-			'ldapAttributesForGroupSearch'];
400
-		foreach ($saKeys as $key) {
401
-			$val = $this->configuration->$key;
402
-			if (is_array($val) && count($val) === 1 && empty($val[0])) {
403
-				$this->configuration->$key = [];
404
-			}
405
-		}
406
-
407
-		if ((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
408
-			&& $this->configuration->ldapTLS) {
409
-			$this->configuration->ldapTLS = false;
410
-			\OCP\Util::writeLog(
411
-				'user_ldap',
412
-				'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
413
-				ILogger::INFO
414
-			);
415
-		}
416
-	}
417
-
418
-	/**
419
-	 * @return bool
420
-	 */
421
-	private function doCriticalValidation() {
422
-		$configurationOK = true;
423
-		$errorStr = 'Configuration Error (prefix '.
424
-			(string)$this->configPrefix .'): ';
425
-
426
-		//options that shall not be empty
427
-		$options = ['ldapHost', 'ldapPort', 'ldapUserDisplayName',
428
-			'ldapGroupDisplayName', 'ldapLoginFilter'];
429
-		foreach ($options as $key) {
430
-			$val = $this->configuration->$key;
431
-			if (empty($val)) {
432
-				switch ($key) {
433
-					case 'ldapHost':
434
-						$subj = 'LDAP Host';
435
-						break;
436
-					case 'ldapPort':
437
-						$subj = 'LDAP Port';
438
-						break;
439
-					case 'ldapUserDisplayName':
440
-						$subj = 'LDAP User Display Name';
441
-						break;
442
-					case 'ldapGroupDisplayName':
443
-						$subj = 'LDAP Group Display Name';
444
-						break;
445
-					case 'ldapLoginFilter':
446
-						$subj = 'LDAP Login Filter';
447
-						break;
448
-					default:
449
-						$subj = $key;
450
-						break;
451
-				}
452
-				$configurationOK = false;
453
-				\OCP\Util::writeLog(
454
-					'user_ldap',
455
-					$errorStr.'No '.$subj.' given!',
456
-					ILogger::WARN
457
-				);
458
-			}
459
-		}
460
-
461
-		//combinations
462
-		$agent = $this->configuration->ldapAgentName;
463
-		$pwd = $this->configuration->ldapAgentPassword;
464
-		if (
465
-			($agent === '' && $pwd !== '')
466
-			|| ($agent !== '' && $pwd === '')
467
-		) {
468
-			\OCP\Util::writeLog(
469
-				'user_ldap',
470
-				$errorStr.'either no password is given for the user ' .
471
-					'agent or a password is given, but not an LDAP agent.',
472
-				ILogger::WARN);
473
-			$configurationOK = false;
474
-		}
475
-
476
-		$base = $this->configuration->ldapBase;
477
-		$baseUsers = $this->configuration->ldapBaseUsers;
478
-		$baseGroups = $this->configuration->ldapBaseGroups;
479
-
480
-		if (empty($base) && empty($baseUsers) && empty($baseGroups)) {
481
-			\OCP\Util::writeLog(
482
-				'user_ldap',
483
-				$errorStr.'Not a single Base DN given.',
484
-				ILogger::WARN
485
-			);
486
-			$configurationOK = false;
487
-		}
488
-
489
-		if (mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
490
-		   === false) {
491
-			\OCP\Util::writeLog(
492
-				'user_ldap',
493
-				$errorStr.'login filter does not contain %uid place holder.',
494
-				ILogger::WARN
495
-			);
496
-			$configurationOK = false;
497
-		}
498
-
499
-		return $configurationOK;
500
-	}
501
-
502
-	/**
503
-	 * Validates the user specified configuration
504
-	 * @return bool true if configuration seems OK, false otherwise
505
-	 */
506
-	private function validateConfiguration() {
507
-		if ($this->doNotValidate) {
508
-			//don't do a validation if it is a new configuration with pure
509
-			//default values. Will be allowed on changes via __set or
510
-			//setConfiguration
511
-			return false;
512
-		}
513
-
514
-		// first step: "soft" checks: settings that are not really
515
-		// necessary, but advisable. If left empty, give an info message
516
-		$this->doSoftValidation();
517
-
518
-		//second step: critical checks. If left empty or filled wrong, mark as
519
-		//not configured and give a warning.
520
-		return $this->doCriticalValidation();
521
-	}
522
-
523
-
524
-	/**
525
-	 * Connects and Binds to LDAP
526
-	 *
527
-	 * @throws ServerNotAvailableException
528
-	 */
529
-	private function establishConnection() {
530
-		if (!$this->configuration->ldapConfigurationActive) {
531
-			return null;
532
-		}
533
-		static $phpLDAPinstalled = true;
534
-		if (!$phpLDAPinstalled) {
535
-			return false;
536
-		}
537
-		if (!$this->ignoreValidation && !$this->configured) {
538
-			\OCP\Util::writeLog(
539
-				'user_ldap',
540
-				'Configuration is invalid, cannot connect',
541
-				ILogger::WARN
542
-			);
543
-			return false;
544
-		}
545
-		if (!$this->ldapConnectionRes) {
546
-			if (!$this->ldap->areLDAPFunctionsAvailable()) {
547
-				$phpLDAPinstalled = false;
548
-				\OCP\Util::writeLog(
549
-					'user_ldap',
550
-					'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
551
-					ILogger::ERROR
552
-				);
553
-
554
-				return false;
555
-			}
556
-			if ($this->configuration->turnOffCertCheck) {
557
-				if (putenv('LDAPTLS_REQCERT=never')) {
558
-					\OCP\Util::writeLog('user_ldap',
559
-						'Turned off SSL certificate validation successfully.',
560
-						ILogger::DEBUG);
561
-				} else {
562
-					\OCP\Util::writeLog(
563
-						'user_ldap',
564
-						'Could not turn off SSL certificate validation.',
565
-						ILogger::WARN
566
-					);
567
-				}
568
-			}
569
-
570
-			$isOverrideMainServer = ($this->configuration->ldapOverrideMainServer
571
-				|| $this->getFromCache('overrideMainServer'));
572
-			$isBackupHost = (trim($this->configuration->ldapBackupHost) !== "");
573
-			$bindStatus = false;
574
-			try {
575
-				if (!$isOverrideMainServer) {
576
-					$this->doConnect($this->configuration->ldapHost,
577
-						$this->configuration->ldapPort);
578
-					return $this->bind();
579
-				}
580
-			} catch (ServerNotAvailableException $e) {
581
-				if (!$isBackupHost) {
582
-					throw $e;
583
-				}
584
-			}
585
-
586
-			//if LDAP server is not reachable, try the Backup (Replica!) Server
587
-			if ($isBackupHost || $isOverrideMainServer) {
588
-				$this->doConnect($this->configuration->ldapBackupHost,
589
-								 $this->configuration->ldapBackupPort);
590
-				$this->bindResult = [];
591
-				$bindStatus = $this->bind();
592
-				$error = $this->ldap->isResource($this->ldapConnectionRes) ?
593
-					$this->ldap->errno($this->ldapConnectionRes) : -1;
594
-				if ($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) {
595
-					//when bind to backup server succeeded and failed to main server,
596
-					//skip contacting him until next cache refresh
597
-					$this->writeToCache('overrideMainServer', true);
598
-				}
599
-			}
600
-
601
-			return $bindStatus;
602
-		}
603
-		return null;
604
-	}
605
-
606
-	/**
607
-	 * @param string $host
608
-	 * @param string $port
609
-	 * @return bool
610
-	 * @throws \OC\ServerNotAvailableException
611
-	 */
612
-	private function doConnect($host, $port) {
613
-		if ($host === '') {
614
-			return false;
615
-		}
616
-
617
-		$this->ldapConnectionRes = $this->ldap->connect($host, $port);
618
-
619
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
620
-			throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
621
-		}
622
-
623
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
624
-			throw new ServerNotAvailableException('Could not disable LDAP referrals.');
625
-		}
626
-
627
-		if ($this->configuration->ldapTLS) {
628
-			if (!$this->ldap->startTls($this->ldapConnectionRes)) {
629
-				throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
630
-			}
631
-		}
632
-
633
-		return true;
634
-	}
635
-
636
-	/**
637
-	 * Binds to LDAP
638
-	 */
639
-	public function bind() {
640
-		if (!$this->configuration->ldapConfigurationActive) {
641
-			return false;
642
-		}
643
-		$cr = $this->ldapConnectionRes;
644
-		if (!$this->ldap->isResource($cr)) {
645
-			$cr = $this->getConnectionResource();
646
-		}
647
-
648
-		if (
649
-			count($this->bindResult) !== 0
650
-			&& $this->bindResult['dn'] === $this->configuration->ldapAgentName
651
-			&& \OC::$server->getHasher()->verify(
652
-				$this->configPrefix . $this->configuration->ldapAgentPassword,
653
-				$this->bindResult['hash']
654
-			)
655
-		) {
656
-			// don't attempt to bind again with the same data as before
657
-			// bind might have been invoked via getConnectionResource(),
658
-			// but we need results specifically for e.g. user login
659
-			return $this->bindResult['result'];
660
-		}
661
-
662
-		$ldapLogin = @$this->ldap->bind($cr,
663
-										$this->configuration->ldapAgentName,
664
-										$this->configuration->ldapAgentPassword);
665
-
666
-		$this->bindResult = [
667
-			'dn' => $this->configuration->ldapAgentName,
668
-			'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword),
669
-			'result' => $ldapLogin,
670
-		];
671
-
672
-		if (!$ldapLogin) {
673
-			$errno = $this->ldap->errno($cr);
674
-
675
-			\OCP\Util::writeLog('user_ldap',
676
-				'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
677
-				ILogger::WARN);
678
-
679
-			// Set to failure mode, if LDAP error code is not LDAP_SUCCESS or LDAP_INVALID_CREDENTIALS
680
-			// or (needed for Apple Open Directory:) LDAP_INSUFFICIENT_ACCESS
681
-			if ($errno !== 0 && $errno !== 49 && $errno !== 50) {
682
-				$this->ldapConnectionRes = null;
683
-			}
684
-
685
-			return false;
686
-		}
687
-		return true;
688
-	}
78
+    private $ldapConnectionRes = null;
79
+    private $configPrefix;
80
+    private $configID;
81
+    private $configured = false;
82
+    //whether connection should be kept on __destruct
83
+    private $dontDestruct = false;
84
+
85
+    /**
86
+     * @var bool runtime flag that indicates whether supported primary groups are available
87
+     */
88
+    public $hasPrimaryGroups = true;
89
+
90
+    /**
91
+     * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
92
+     */
93
+    public $hasGidNumber = true;
94
+
95
+    //cache handler
96
+    protected $cache;
97
+
98
+    /** @var Configuration settings handler **/
99
+    protected $configuration;
100
+
101
+    protected $doNotValidate = false;
102
+
103
+    protected $ignoreValidation = false;
104
+
105
+    protected $bindResult = [];
106
+
107
+    /**
108
+     * Constructor
109
+     * @param ILDAPWrapper $ldap
110
+     * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
111
+     * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
112
+     */
113
+    public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') {
114
+        parent::__construct($ldap);
115
+        $this->configPrefix = $configPrefix;
116
+        $this->configID = $configID;
117
+        $this->configuration = new Configuration($configPrefix,
118
+                                                    !is_null($configID));
119
+        $memcache = \OC::$server->getMemCacheFactory();
120
+        if ($memcache->isAvailable()) {
121
+            $this->cache = $memcache->createDistributed();
122
+        }
123
+        $helper = new Helper(\OC::$server->getConfig());
124
+        $this->doNotValidate = !in_array($this->configPrefix,
125
+            $helper->getServerConfigurationPrefixes());
126
+    }
127
+
128
+    public function __destruct() {
129
+        if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
130
+            @$this->ldap->unbind($this->ldapConnectionRes);
131
+            $this->bindResult = [];
132
+        }
133
+    }
134
+
135
+    /**
136
+     * defines behaviour when the instance is cloned
137
+     */
138
+    public function __clone() {
139
+        $this->configuration = new Configuration($this->configPrefix,
140
+                                                    !is_null($this->configID));
141
+        if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
142
+            $this->bindResult = [];
143
+        }
144
+        $this->ldapConnectionRes = null;
145
+        $this->dontDestruct = true;
146
+    }
147
+
148
+    public function __get(string $name) {
149
+        if (!$this->configured) {
150
+            $this->readConfiguration();
151
+        }
152
+
153
+        return $this->configuration->$name;
154
+    }
155
+
156
+    /**
157
+     * @param string $name
158
+     * @param mixed $value
159
+     */
160
+    public function __set($name, $value) {
161
+        $this->doNotValidate = false;
162
+        $before = $this->configuration->$name;
163
+        $this->configuration->$name = $value;
164
+        $after = $this->configuration->$name;
165
+        if ($before !== $after) {
166
+            if ($this->configID !== '' && $this->configID !== null) {
167
+                $this->configuration->saveConfiguration();
168
+            }
169
+            $this->validateConfiguration();
170
+        }
171
+    }
172
+
173
+    /**
174
+     * @param string $rule
175
+     * @return array
176
+     * @throws \RuntimeException
177
+     */
178
+    public function resolveRule($rule) {
179
+        return $this->configuration->resolveRule($rule);
180
+    }
181
+
182
+    /**
183
+     * sets whether the result of the configuration validation shall
184
+     * be ignored when establishing the connection. Used by the Wizard
185
+     * in early configuration state.
186
+     * @param bool $state
187
+     */
188
+    public function setIgnoreValidation($state) {
189
+        $this->ignoreValidation = (bool)$state;
190
+    }
191
+
192
+    /**
193
+     * initializes the LDAP backend
194
+     * @param bool $force read the config settings no matter what
195
+     */
196
+    public function init($force = false) {
197
+        $this->readConfiguration($force);
198
+        $this->establishConnection();
199
+    }
200
+
201
+    /**
202
+     * Returns the LDAP handler
203
+     */
204
+    public function getConnectionResource() {
205
+        if (!$this->ldapConnectionRes) {
206
+            $this->init();
207
+        } elseif (!$this->ldap->isResource($this->ldapConnectionRes)) {
208
+            $this->ldapConnectionRes = null;
209
+            $this->establishConnection();
210
+        }
211
+        if (is_null($this->ldapConnectionRes)) {
212
+            \OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, ILogger::ERROR);
213
+            throw new ServerNotAvailableException('Connection to LDAP server could not be established');
214
+        }
215
+        return $this->ldapConnectionRes;
216
+    }
217
+
218
+    /**
219
+     * resets the connection resource
220
+     */
221
+    public function resetConnectionResource() {
222
+        if (!is_null($this->ldapConnectionRes)) {
223
+            @$this->ldap->unbind($this->ldapConnectionRes);
224
+            $this->ldapConnectionRes = null;
225
+            $this->bindResult = [];
226
+        }
227
+    }
228
+
229
+    /**
230
+     * @param string|null $key
231
+     * @return string
232
+     */
233
+    private function getCacheKey($key) {
234
+        $prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
235
+        if (is_null($key)) {
236
+            return $prefix;
237
+        }
238
+        return $prefix.hash('sha256', $key);
239
+    }
240
+
241
+    /**
242
+     * @param string $key
243
+     * @return mixed|null
244
+     */
245
+    public function getFromCache($key) {
246
+        if (!$this->configured) {
247
+            $this->readConfiguration();
248
+        }
249
+        if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
250
+            return null;
251
+        }
252
+        $key = $this->getCacheKey($key);
253
+
254
+        return json_decode(base64_decode($this->cache->get($key)), true);
255
+    }
256
+
257
+    /**
258
+     * @param string $key
259
+     * @param mixed $value
260
+     *
261
+     * @return string
262
+     */
263
+    public function writeToCache($key, $value) {
264
+        if (!$this->configured) {
265
+            $this->readConfiguration();
266
+        }
267
+        if (is_null($this->cache)
268
+            || !$this->configuration->ldapCacheTTL
269
+            || !$this->configuration->ldapConfigurationActive) {
270
+            return null;
271
+        }
272
+        $key = $this->getCacheKey($key);
273
+        $value = base64_encode(json_encode($value));
274
+        $this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
275
+    }
276
+
277
+    public function clearCache() {
278
+        if (!is_null($this->cache)) {
279
+            $this->cache->clear($this->getCacheKey(null));
280
+        }
281
+    }
282
+
283
+    /**
284
+     * Caches the general LDAP configuration.
285
+     * @param bool $force optional. true, if the re-read should be forced. defaults
286
+     * to false.
287
+     * @return null
288
+     */
289
+    private function readConfiguration($force = false) {
290
+        if ((!$this->configured || $force) && !is_null($this->configID)) {
291
+            $this->configuration->readConfiguration();
292
+            $this->configured = $this->validateConfiguration();
293
+        }
294
+    }
295
+
296
+    /**
297
+     * set LDAP configuration with values delivered by an array, not read from configuration
298
+     * @param array $config array that holds the config parameters in an associated array
299
+     * @param array &$setParameters optional; array where the set fields will be given to
300
+     * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
301
+     */
302
+    public function setConfiguration($config, &$setParameters = null) {
303
+        if (is_null($setParameters)) {
304
+            $setParameters = [];
305
+        }
306
+        $this->doNotValidate = false;
307
+        $this->configuration->setConfiguration($config, $setParameters);
308
+        if (count($setParameters) > 0) {
309
+            $this->configured = $this->validateConfiguration();
310
+        }
311
+
312
+
313
+        return $this->configured;
314
+    }
315
+
316
+    /**
317
+     * saves the current Configuration in the database and empties the
318
+     * cache
319
+     * @return null
320
+     */
321
+    public function saveConfiguration() {
322
+        $this->configuration->saveConfiguration();
323
+        $this->clearCache();
324
+    }
325
+
326
+    /**
327
+     * get the current LDAP configuration
328
+     * @return array
329
+     */
330
+    public function getConfiguration() {
331
+        $this->readConfiguration();
332
+        $config = $this->configuration->getConfiguration();
333
+        $cta = $this->configuration->getConfigTranslationArray();
334
+        $result = [];
335
+        foreach ($cta as $dbkey => $configkey) {
336
+            switch ($configkey) {
337
+                case 'homeFolderNamingRule':
338
+                    if (strpos($config[$configkey], 'attr:') === 0) {
339
+                        $result[$dbkey] = substr($config[$configkey], 5);
340
+                    } else {
341
+                        $result[$dbkey] = '';
342
+                    }
343
+                    break;
344
+                case 'ldapBase':
345
+                case 'ldapBaseUsers':
346
+                case 'ldapBaseGroups':
347
+                case 'ldapAttributesForUserSearch':
348
+                case 'ldapAttributesForGroupSearch':
349
+                    if (is_array($config[$configkey])) {
350
+                        $result[$dbkey] = implode("\n", $config[$configkey]);
351
+                        break;
352
+                    } //else follows default
353
+                    // no break
354
+                default:
355
+                    $result[$dbkey] = $config[$configkey];
356
+            }
357
+        }
358
+        return $result;
359
+    }
360
+
361
+    private function doSoftValidation() {
362
+        //if User or Group Base are not set, take over Base DN setting
363
+        foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
364
+            $val = $this->configuration->$keyBase;
365
+            if (empty($val)) {
366
+                $this->configuration->$keyBase = $this->configuration->ldapBase;
367
+            }
368
+        }
369
+
370
+        foreach (['ldapExpertUUIDUserAttr' => 'ldapUuidUserAttribute',
371
+            'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute']
372
+                as $expertSetting => $effectiveSetting) {
373
+            $uuidOverride = $this->configuration->$expertSetting;
374
+            if (!empty($uuidOverride)) {
375
+                $this->configuration->$effectiveSetting = $uuidOverride;
376
+            } else {
377
+                $uuidAttributes = Access::UUID_ATTRIBUTES;
378
+                array_unshift($uuidAttributes, 'auto');
379
+                if (!in_array($this->configuration->$effectiveSetting,
380
+                            $uuidAttributes)
381
+                    && (!is_null($this->configID))) {
382
+                    $this->configuration->$effectiveSetting = 'auto';
383
+                    $this->configuration->saveConfiguration();
384
+                    \OCP\Util::writeLog('user_ldap',
385
+                                        'Illegal value for the '.
386
+                                        $effectiveSetting.', '.'reset to '.
387
+                                        'autodetect.', ILogger::INFO);
388
+                }
389
+            }
390
+        }
391
+
392
+        $backupPort = (int)$this->configuration->ldapBackupPort;
393
+        if ($backupPort <= 0) {
394
+            $this->configuration->backupPort = $this->configuration->ldapPort;
395
+        }
396
+
397
+        //make sure empty search attributes are saved as simple, empty array
398
+        $saKeys = ['ldapAttributesForUserSearch',
399
+            'ldapAttributesForGroupSearch'];
400
+        foreach ($saKeys as $key) {
401
+            $val = $this->configuration->$key;
402
+            if (is_array($val) && count($val) === 1 && empty($val[0])) {
403
+                $this->configuration->$key = [];
404
+            }
405
+        }
406
+
407
+        if ((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
408
+            && $this->configuration->ldapTLS) {
409
+            $this->configuration->ldapTLS = false;
410
+            \OCP\Util::writeLog(
411
+                'user_ldap',
412
+                'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
413
+                ILogger::INFO
414
+            );
415
+        }
416
+    }
417
+
418
+    /**
419
+     * @return bool
420
+     */
421
+    private function doCriticalValidation() {
422
+        $configurationOK = true;
423
+        $errorStr = 'Configuration Error (prefix '.
424
+            (string)$this->configPrefix .'): ';
425
+
426
+        //options that shall not be empty
427
+        $options = ['ldapHost', 'ldapPort', 'ldapUserDisplayName',
428
+            'ldapGroupDisplayName', 'ldapLoginFilter'];
429
+        foreach ($options as $key) {
430
+            $val = $this->configuration->$key;
431
+            if (empty($val)) {
432
+                switch ($key) {
433
+                    case 'ldapHost':
434
+                        $subj = 'LDAP Host';
435
+                        break;
436
+                    case 'ldapPort':
437
+                        $subj = 'LDAP Port';
438
+                        break;
439
+                    case 'ldapUserDisplayName':
440
+                        $subj = 'LDAP User Display Name';
441
+                        break;
442
+                    case 'ldapGroupDisplayName':
443
+                        $subj = 'LDAP Group Display Name';
444
+                        break;
445
+                    case 'ldapLoginFilter':
446
+                        $subj = 'LDAP Login Filter';
447
+                        break;
448
+                    default:
449
+                        $subj = $key;
450
+                        break;
451
+                }
452
+                $configurationOK = false;
453
+                \OCP\Util::writeLog(
454
+                    'user_ldap',
455
+                    $errorStr.'No '.$subj.' given!',
456
+                    ILogger::WARN
457
+                );
458
+            }
459
+        }
460
+
461
+        //combinations
462
+        $agent = $this->configuration->ldapAgentName;
463
+        $pwd = $this->configuration->ldapAgentPassword;
464
+        if (
465
+            ($agent === '' && $pwd !== '')
466
+            || ($agent !== '' && $pwd === '')
467
+        ) {
468
+            \OCP\Util::writeLog(
469
+                'user_ldap',
470
+                $errorStr.'either no password is given for the user ' .
471
+                    'agent or a password is given, but not an LDAP agent.',
472
+                ILogger::WARN);
473
+            $configurationOK = false;
474
+        }
475
+
476
+        $base = $this->configuration->ldapBase;
477
+        $baseUsers = $this->configuration->ldapBaseUsers;
478
+        $baseGroups = $this->configuration->ldapBaseGroups;
479
+
480
+        if (empty($base) && empty($baseUsers) && empty($baseGroups)) {
481
+            \OCP\Util::writeLog(
482
+                'user_ldap',
483
+                $errorStr.'Not a single Base DN given.',
484
+                ILogger::WARN
485
+            );
486
+            $configurationOK = false;
487
+        }
488
+
489
+        if (mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
490
+            === false) {
491
+            \OCP\Util::writeLog(
492
+                'user_ldap',
493
+                $errorStr.'login filter does not contain %uid place holder.',
494
+                ILogger::WARN
495
+            );
496
+            $configurationOK = false;
497
+        }
498
+
499
+        return $configurationOK;
500
+    }
501
+
502
+    /**
503
+     * Validates the user specified configuration
504
+     * @return bool true if configuration seems OK, false otherwise
505
+     */
506
+    private function validateConfiguration() {
507
+        if ($this->doNotValidate) {
508
+            //don't do a validation if it is a new configuration with pure
509
+            //default values. Will be allowed on changes via __set or
510
+            //setConfiguration
511
+            return false;
512
+        }
513
+
514
+        // first step: "soft" checks: settings that are not really
515
+        // necessary, but advisable. If left empty, give an info message
516
+        $this->doSoftValidation();
517
+
518
+        //second step: critical checks. If left empty or filled wrong, mark as
519
+        //not configured and give a warning.
520
+        return $this->doCriticalValidation();
521
+    }
522
+
523
+
524
+    /**
525
+     * Connects and Binds to LDAP
526
+     *
527
+     * @throws ServerNotAvailableException
528
+     */
529
+    private function establishConnection() {
530
+        if (!$this->configuration->ldapConfigurationActive) {
531
+            return null;
532
+        }
533
+        static $phpLDAPinstalled = true;
534
+        if (!$phpLDAPinstalled) {
535
+            return false;
536
+        }
537
+        if (!$this->ignoreValidation && !$this->configured) {
538
+            \OCP\Util::writeLog(
539
+                'user_ldap',
540
+                'Configuration is invalid, cannot connect',
541
+                ILogger::WARN
542
+            );
543
+            return false;
544
+        }
545
+        if (!$this->ldapConnectionRes) {
546
+            if (!$this->ldap->areLDAPFunctionsAvailable()) {
547
+                $phpLDAPinstalled = false;
548
+                \OCP\Util::writeLog(
549
+                    'user_ldap',
550
+                    'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
551
+                    ILogger::ERROR
552
+                );
553
+
554
+                return false;
555
+            }
556
+            if ($this->configuration->turnOffCertCheck) {
557
+                if (putenv('LDAPTLS_REQCERT=never')) {
558
+                    \OCP\Util::writeLog('user_ldap',
559
+                        'Turned off SSL certificate validation successfully.',
560
+                        ILogger::DEBUG);
561
+                } else {
562
+                    \OCP\Util::writeLog(
563
+                        'user_ldap',
564
+                        'Could not turn off SSL certificate validation.',
565
+                        ILogger::WARN
566
+                    );
567
+                }
568
+            }
569
+
570
+            $isOverrideMainServer = ($this->configuration->ldapOverrideMainServer
571
+                || $this->getFromCache('overrideMainServer'));
572
+            $isBackupHost = (trim($this->configuration->ldapBackupHost) !== "");
573
+            $bindStatus = false;
574
+            try {
575
+                if (!$isOverrideMainServer) {
576
+                    $this->doConnect($this->configuration->ldapHost,
577
+                        $this->configuration->ldapPort);
578
+                    return $this->bind();
579
+                }
580
+            } catch (ServerNotAvailableException $e) {
581
+                if (!$isBackupHost) {
582
+                    throw $e;
583
+                }
584
+            }
585
+
586
+            //if LDAP server is not reachable, try the Backup (Replica!) Server
587
+            if ($isBackupHost || $isOverrideMainServer) {
588
+                $this->doConnect($this->configuration->ldapBackupHost,
589
+                                    $this->configuration->ldapBackupPort);
590
+                $this->bindResult = [];
591
+                $bindStatus = $this->bind();
592
+                $error = $this->ldap->isResource($this->ldapConnectionRes) ?
593
+                    $this->ldap->errno($this->ldapConnectionRes) : -1;
594
+                if ($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) {
595
+                    //when bind to backup server succeeded and failed to main server,
596
+                    //skip contacting him until next cache refresh
597
+                    $this->writeToCache('overrideMainServer', true);
598
+                }
599
+            }
600
+
601
+            return $bindStatus;
602
+        }
603
+        return null;
604
+    }
605
+
606
+    /**
607
+     * @param string $host
608
+     * @param string $port
609
+     * @return bool
610
+     * @throws \OC\ServerNotAvailableException
611
+     */
612
+    private function doConnect($host, $port) {
613
+        if ($host === '') {
614
+            return false;
615
+        }
616
+
617
+        $this->ldapConnectionRes = $this->ldap->connect($host, $port);
618
+
619
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
620
+            throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
621
+        }
622
+
623
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
624
+            throw new ServerNotAvailableException('Could not disable LDAP referrals.');
625
+        }
626
+
627
+        if ($this->configuration->ldapTLS) {
628
+            if (!$this->ldap->startTls($this->ldapConnectionRes)) {
629
+                throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
630
+            }
631
+        }
632
+
633
+        return true;
634
+    }
635
+
636
+    /**
637
+     * Binds to LDAP
638
+     */
639
+    public function bind() {
640
+        if (!$this->configuration->ldapConfigurationActive) {
641
+            return false;
642
+        }
643
+        $cr = $this->ldapConnectionRes;
644
+        if (!$this->ldap->isResource($cr)) {
645
+            $cr = $this->getConnectionResource();
646
+        }
647
+
648
+        if (
649
+            count($this->bindResult) !== 0
650
+            && $this->bindResult['dn'] === $this->configuration->ldapAgentName
651
+            && \OC::$server->getHasher()->verify(
652
+                $this->configPrefix . $this->configuration->ldapAgentPassword,
653
+                $this->bindResult['hash']
654
+            )
655
+        ) {
656
+            // don't attempt to bind again with the same data as before
657
+            // bind might have been invoked via getConnectionResource(),
658
+            // but we need results specifically for e.g. user login
659
+            return $this->bindResult['result'];
660
+        }
661
+
662
+        $ldapLogin = @$this->ldap->bind($cr,
663
+                                        $this->configuration->ldapAgentName,
664
+                                        $this->configuration->ldapAgentPassword);
665
+
666
+        $this->bindResult = [
667
+            'dn' => $this->configuration->ldapAgentName,
668
+            'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword),
669
+            'result' => $ldapLogin,
670
+        ];
671
+
672
+        if (!$ldapLogin) {
673
+            $errno = $this->ldap->errno($cr);
674
+
675
+            \OCP\Util::writeLog('user_ldap',
676
+                'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
677
+                ILogger::WARN);
678
+
679
+            // Set to failure mode, if LDAP error code is not LDAP_SUCCESS or LDAP_INVALID_CREDENTIALS
680
+            // or (needed for Apple Open Directory:) LDAP_INSUFFICIENT_ACCESS
681
+            if ($errno !== 0 && $errno !== 49 && $errno !== 50) {
682
+                $this->ldapConnectionRes = null;
683
+            }
684
+
685
+            return false;
686
+        }
687
+        return true;
688
+    }
689 689
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Group_LDAP.php 2 patches
Indentation   +1268 added lines, -1268 removed lines patch added patch discarded remove patch
@@ -53,1272 +53,1272 @@
 block discarded – undo
53 53
 use OCP\ILogger;
54 54
 
55 55
 class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
56
-	protected $enabled = false;
57
-
58
-	/** @var string[] $cachedGroupMembers array of users with gid as key */
59
-	protected $cachedGroupMembers;
60
-	/** @var string[] $cachedGroupsByMember array of groups with uid as key */
61
-	protected $cachedGroupsByMember;
62
-	/** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
63
-	protected $cachedNestedGroups;
64
-	/** @var GroupPluginManager */
65
-	protected $groupPluginManager;
66
-	/** @var ILogger */
67
-	protected $logger;
68
-
69
-	/**
70
-	 * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
71
-	 */
72
-	protected $ldapGroupMemberAssocAttr;
73
-
74
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
75
-		parent::__construct($access);
76
-		$filter = $this->access->connection->ldapGroupFilter;
77
-		$gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
78
-		if (!empty($filter) && !empty($gAssoc)) {
79
-			$this->enabled = true;
80
-		}
81
-
82
-		$this->cachedGroupMembers = new CappedMemoryCache();
83
-		$this->cachedGroupsByMember = new CappedMemoryCache();
84
-		$this->cachedNestedGroups = new CappedMemoryCache();
85
-		$this->groupPluginManager = $groupPluginManager;
86
-		$this->logger = OC::$server->getLogger();
87
-		$this->ldapGroupMemberAssocAttr = strtolower($gAssoc);
88
-	}
89
-
90
-	/**
91
-	 * is user in group?
92
-	 *
93
-	 * @param string $uid uid of the user
94
-	 * @param string $gid gid of the group
95
-	 * @return bool
96
-	 * @throws Exception
97
-	 * @throws ServerNotAvailableException
98
-	 */
99
-	public function inGroup($uid, $gid) {
100
-		if (!$this->enabled) {
101
-			return false;
102
-		}
103
-		$cacheKey = 'inGroup' . $uid . ':' . $gid;
104
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
105
-		if (!is_null($inGroup)) {
106
-			return (bool)$inGroup;
107
-		}
108
-
109
-		$userDN = $this->access->username2dn($uid);
110
-
111
-		if (isset($this->cachedGroupMembers[$gid])) {
112
-			return in_array($userDN, $this->cachedGroupMembers[$gid]);
113
-		}
114
-
115
-		$cacheKeyMembers = 'inGroup-members:' . $gid;
116
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
117
-		if (!is_null($members)) {
118
-			$this->cachedGroupMembers[$gid] = $members;
119
-			$isInGroup = in_array($userDN, $members, true);
120
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
121
-			return $isInGroup;
122
-		}
123
-
124
-		$groupDN = $this->access->groupname2dn($gid);
125
-		// just in case
126
-		if (!$groupDN || !$userDN) {
127
-			$this->access->connection->writeToCache($cacheKey, false);
128
-			return false;
129
-		}
130
-
131
-		//check primary group first
132
-		if ($gid === $this->getUserPrimaryGroup($userDN)) {
133
-			$this->access->connection->writeToCache($cacheKey, true);
134
-			return true;
135
-		}
136
-
137
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
138
-		$members = $this->_groupMembers($groupDN);
139
-		if (!is_array($members) || count($members) === 0) {
140
-			$this->access->connection->writeToCache($cacheKey, false);
141
-			return false;
142
-		}
143
-
144
-		//extra work if we don't get back user DNs
145
-		switch ($this->ldapGroupMemberAssocAttr) {
146
-			case 'memberuid':
147
-			case 'zimbramailforwardingaddress':
148
-				$requestAttributes = $this->access->userManager->getAttributes(true);
149
-				$dns = [];
150
-				$filterParts = [];
151
-				$bytes = 0;
152
-				foreach ($members as $mid) {
153
-					if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
154
-						$parts = explode('@', $mid); //making sure we get only the uid
155
-						$mid = $parts[0];
156
-					}
157
-					$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
158
-					$filterParts[] = $filter;
159
-					$bytes += strlen($filter);
160
-					if ($bytes >= 9000000) {
161
-						// AD has a default input buffer of 10 MB, we do not want
162
-						// to take even the chance to exceed it
163
-						$filter = $this->access->combineFilterWithOr($filterParts);
164
-						$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
165
-						$bytes = 0;
166
-						$filterParts = [];
167
-						$dns = array_merge($dns, $users);
168
-					}
169
-				}
170
-				if (count($filterParts) > 0) {
171
-					$filter = $this->access->combineFilterWithOr($filterParts);
172
-					$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
173
-					$dns = array_merge($dns, $users);
174
-				}
175
-				$members = $dns;
176
-				break;
177
-		}
178
-
179
-		$isInGroup = in_array($userDN, $members);
180
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
181
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
182
-		$this->cachedGroupMembers[$gid] = $members;
183
-
184
-		return $isInGroup;
185
-	}
186
-
187
-	/**
188
-	 * For a group that has user membership defined by an LDAP search url
189
-	 * attribute returns the users that match the search url otherwise returns
190
-	 * an empty array.
191
-	 *
192
-	 * @throws ServerNotAvailableException
193
-	 */
194
-	public function getDynamicGroupMembers(string $dnGroup): array {
195
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
196
-
197
-		if (empty($dynamicGroupMemberURL)) {
198
-			return [];
199
-		}
200
-
201
-		$dynamicMembers = [];
202
-		$memberURLs = $this->access->readAttribute(
203
-			$dnGroup,
204
-			$dynamicGroupMemberURL,
205
-			$this->access->connection->ldapGroupFilter
206
-		);
207
-		if ($memberURLs !== false) {
208
-			// this group has the 'memberURL' attribute so this is a dynamic group
209
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
210
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
211
-			$pos = strpos($memberURLs[0], '(');
212
-			if ($pos !== false) {
213
-				$memberUrlFilter = substr($memberURLs[0], $pos);
214
-				$foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
215
-				$dynamicMembers = [];
216
-				foreach ($foundMembers as $value) {
217
-					$dynamicMembers[$value['dn'][0]] = 1;
218
-				}
219
-			} else {
220
-				$this->logger->debug('No search filter found on member url of group {dn}',
221
-					[
222
-						'app' => 'user_ldap',
223
-						'dn' => $dnGroup,
224
-					]
225
-				);
226
-			}
227
-		}
228
-		return $dynamicMembers;
229
-	}
230
-
231
-	/**
232
-	 * @throws ServerNotAvailableException
233
-	 */
234
-	private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
235
-		if ($seen === null) {
236
-			$seen = [];
237
-		}
238
-		$allMembers = [];
239
-		if (array_key_exists($dnGroup, $seen)) {
240
-			return [];
241
-		}
242
-		// used extensively in cron job, caching makes sense for nested groups
243
-		$cacheKey = '_groupMembers' . $dnGroup;
244
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
245
-		if ($groupMembers !== null) {
246
-			return $groupMembers;
247
-		}
248
-
249
-		if ($this->access->connection->ldapNestedGroups
250
-			&& $this->access->connection->useMemberOfToDetectMembership
251
-			&& $this->access->connection->hasMemberOfFilterSupport
252
-			&& $this->access->connection->ldapMatchingRuleInChainState !== Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE
253
-		) {
254
-			$attemptedLdapMatchingRuleInChain = true;
255
-			// compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
256
-			$filter = $this->access->combineFilterWithAnd([
257
-				$this->access->connection->ldapUserFilter,
258
-				$this->access->connection->ldapUserDisplayName . '=*',
259
-				'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
260
-			]);
261
-			$memberRecords = $this->access->fetchListOfUsers(
262
-				$filter,
263
-				$this->access->userManager->getAttributes(true)
264
-			);
265
-			$result = array_reduce($memberRecords, function ($carry, $record) {
266
-				$carry[] = $record['dn'][0];
267
-				return $carry;
268
-			}, []);
269
-			if ($this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_AVAILABLE) {
270
-				return $result;
271
-			} elseif (!empty($memberRecords)) {
272
-				$this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_AVAILABLE;
273
-				$this->access->connection->saveConfiguration();
274
-				return $result;
275
-			}
276
-			// when feature availability is unknown, and the result is empty, continue and test with original approach
277
-		}
278
-
279
-		$seen[$dnGroup] = 1;
280
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
281
-		if (is_array($members)) {
282
-			$fetcher = function ($memberDN, &$seen) {
283
-				return $this->_groupMembers($memberDN, $seen);
284
-			};
285
-			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
286
-		}
287
-
288
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
289
-
290
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
291
-		if (isset($attemptedLdapMatchingRuleInChain)
292
-			&& $this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_UNKNOWN
293
-			&& !empty($allMembers)
294
-		) {
295
-			$this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE;
296
-			$this->access->connection->saveConfiguration();
297
-		}
298
-		return $allMembers;
299
-	}
300
-
301
-	/**
302
-	 * @throws ServerNotAvailableException
303
-	 */
304
-	private function _getGroupDNsFromMemberOf(string $dn): array {
305
-		$groups = $this->access->readAttribute($dn, 'memberOf');
306
-		if (!is_array($groups)) {
307
-			return [];
308
-		}
309
-
310
-		$fetcher = function ($groupDN) {
311
-			if (isset($this->cachedNestedGroups[$groupDN])) {
312
-				$nestedGroups = $this->cachedNestedGroups[$groupDN];
313
-			} else {
314
-				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
315
-				if (!is_array($nestedGroups)) {
316
-					$nestedGroups = [];
317
-				}
318
-				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
319
-			}
320
-			return $nestedGroups;
321
-		};
322
-
323
-		$groups = $this->walkNestedGroups($dn, $fetcher, $groups);
324
-		return $this->filterValidGroups($groups);
325
-	}
326
-
327
-	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
328
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
329
-		// depending on the input, we either have a list of DNs or a list of LDAP records
330
-		// also, the output expects either DNs or records. Testing the first element should suffice.
331
-		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
332
-
333
-		if ($nesting !== 1) {
334
-			if ($recordMode) {
335
-				// the keys are numeric, but should hold the DN
336
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
337
-					if ($record['dn'][0] != $dn) {
338
-						$transformed[$record['dn'][0]] = $record;
339
-					}
340
-					return $transformed;
341
-				}, []);
342
-			}
343
-			return $list;
344
-		}
345
-
346
-		$seen = [];
347
-		while ($record = array_shift($list)) {
348
-			$recordDN = $recordMode ? $record['dn'][0] : $record;
349
-			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
350
-				// Prevent loops
351
-				continue;
352
-			}
353
-			$fetched = $fetcher($record, $seen);
354
-			$list = array_merge($list, $fetched);
355
-			$seen[$recordDN] = $record;
356
-		}
357
-
358
-		return $recordMode ? $seen : array_keys($seen);
359
-	}
360
-
361
-	/**
362
-	 * translates a gidNumber into an ownCloud internal name
363
-	 *
364
-	 * @return string|bool
365
-	 * @throws Exception
366
-	 * @throws ServerNotAvailableException
367
-	 */
368
-	public function gidNumber2Name(string $gid, string $dn) {
369
-		$cacheKey = 'gidNumberToName' . $gid;
370
-		$groupName = $this->access->connection->getFromCache($cacheKey);
371
-		if (!is_null($groupName) && isset($groupName)) {
372
-			return $groupName;
373
-		}
374
-
375
-		//we need to get the DN from LDAP
376
-		$filter = $this->access->combineFilterWithAnd([
377
-			$this->access->connection->ldapGroupFilter,
378
-			'objectClass=posixGroup',
379
-			$this->access->connection->ldapGidNumber . '=' . $gid
380
-		]);
381
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
382
-	}
383
-
384
-	/**
385
-	 * @throws ServerNotAvailableException
386
-	 * @throws Exception
387
-	 */
388
-	private function getNameOfGroup(string $filter, string $cacheKey) {
389
-		$result = $this->access->searchGroups($filter, ['dn'], 1);
390
-		if (empty($result)) {
391
-			return null;
392
-		}
393
-		$dn = $result[0]['dn'][0];
394
-
395
-		//and now the group name
396
-		//NOTE once we have separate Nextcloud group IDs and group names we can
397
-		//directly read the display name attribute instead of the DN
398
-		$name = $this->access->dn2groupname($dn);
399
-
400
-		$this->access->connection->writeToCache($cacheKey, $name);
401
-
402
-		return $name;
403
-	}
404
-
405
-	/**
406
-	 * returns the entry's gidNumber
407
-	 *
408
-	 * @return string|bool
409
-	 * @throws ServerNotAvailableException
410
-	 */
411
-	private function getEntryGidNumber(string $dn, string $attribute) {
412
-		$value = $this->access->readAttribute($dn, $attribute);
413
-		if (is_array($value) && !empty($value)) {
414
-			return $value[0];
415
-		}
416
-		return false;
417
-	}
418
-
419
-	/**
420
-	 * @return string|bool
421
-	 * @throws ServerNotAvailableException
422
-	 */
423
-	public function getGroupGidNumber(string $dn) {
424
-		return $this->getEntryGidNumber($dn, 'gidNumber');
425
-	}
426
-
427
-	/**
428
-	 * returns the user's gidNumber
429
-	 *
430
-	 * @return string|bool
431
-	 * @throws ServerNotAvailableException
432
-	 */
433
-	public function getUserGidNumber(string $dn) {
434
-		$gidNumber = false;
435
-		if ($this->access->connection->hasGidNumber) {
436
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
437
-			if ($gidNumber === false) {
438
-				$this->access->connection->hasGidNumber = false;
439
-			}
440
-		}
441
-		return $gidNumber;
442
-	}
443
-
444
-	/**
445
-	 * @throws ServerNotAvailableException
446
-	 * @throws Exception
447
-	 */
448
-	private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
449
-		$groupID = $this->getGroupGidNumber($groupDN);
450
-		if ($groupID === false) {
451
-			throw new Exception('Not a valid group');
452
-		}
453
-
454
-		$filterParts = [];
455
-		$filterParts[] = $this->access->getFilterForUserCount();
456
-		if ($search !== '') {
457
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
458
-		}
459
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
460
-
461
-		return $this->access->combineFilterWithAnd($filterParts);
462
-	}
463
-
464
-	/**
465
-	 * returns a list of users that have the given group as gid number
466
-	 *
467
-	 * @throws ServerNotAvailableException
468
-	 */
469
-	public function getUsersInGidNumber(
470
-		string $groupDN,
471
-		string $search = '',
472
-		?int $limit = -1,
473
-		?int $offset = 0
474
-	): array {
475
-		try {
476
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
477
-			$users = $this->access->fetchListOfUsers(
478
-				$filter,
479
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
480
-				$limit,
481
-				$offset
482
-			);
483
-			return $this->access->nextcloudUserNames($users);
484
-		} catch (ServerNotAvailableException $e) {
485
-			throw $e;
486
-		} catch (Exception $e) {
487
-			return [];
488
-		}
489
-	}
490
-
491
-	/**
492
-	 * @throws ServerNotAvailableException
493
-	 * @return bool
494
-	 */
495
-	public function getUserGroupByGid(string $dn) {
496
-		$groupID = $this->getUserGidNumber($dn);
497
-		if ($groupID !== false) {
498
-			$groupName = $this->gidNumber2Name($groupID, $dn);
499
-			if ($groupName !== false) {
500
-				return $groupName;
501
-			}
502
-		}
503
-
504
-		return false;
505
-	}
506
-
507
-	/**
508
-	 * translates a primary group ID into an Nextcloud internal name
509
-	 *
510
-	 * @return string|bool
511
-	 * @throws Exception
512
-	 * @throws ServerNotAvailableException
513
-	 */
514
-	public function primaryGroupID2Name(string $gid, string $dn) {
515
-		$cacheKey = 'primaryGroupIDtoName';
516
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
517
-		if (!is_null($groupNames) && isset($groupNames[$gid])) {
518
-			return $groupNames[$gid];
519
-		}
520
-
521
-		$domainObjectSid = $this->access->getSID($dn);
522
-		if ($domainObjectSid === false) {
523
-			return false;
524
-		}
525
-
526
-		//we need to get the DN from LDAP
527
-		$filter = $this->access->combineFilterWithAnd([
528
-			$this->access->connection->ldapGroupFilter,
529
-			'objectsid=' . $domainObjectSid . '-' . $gid
530
-		]);
531
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
532
-	}
533
-
534
-	/**
535
-	 * returns the entry's primary group ID
536
-	 *
537
-	 * @return string|bool
538
-	 * @throws ServerNotAvailableException
539
-	 */
540
-	private function getEntryGroupID(string $dn, string $attribute) {
541
-		$value = $this->access->readAttribute($dn, $attribute);
542
-		if (is_array($value) && !empty($value)) {
543
-			return $value[0];
544
-		}
545
-		return false;
546
-	}
547
-
548
-	/**
549
-	 * @return string|bool
550
-	 * @throws ServerNotAvailableException
551
-	 */
552
-	public function getGroupPrimaryGroupID(string $dn) {
553
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
554
-	}
555
-
556
-	/**
557
-	 * @return string|bool
558
-	 * @throws ServerNotAvailableException
559
-	 */
560
-	public function getUserPrimaryGroupIDs(string $dn) {
561
-		$primaryGroupID = false;
562
-		if ($this->access->connection->hasPrimaryGroups) {
563
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
564
-			if ($primaryGroupID === false) {
565
-				$this->access->connection->hasPrimaryGroups = false;
566
-			}
567
-		}
568
-		return $primaryGroupID;
569
-	}
570
-
571
-	/**
572
-	 * @throws Exception
573
-	 * @throws ServerNotAvailableException
574
-	 */
575
-	private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
576
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
577
-		if ($groupID === false) {
578
-			throw new Exception('Not a valid group');
579
-		}
580
-
581
-		$filterParts = [];
582
-		$filterParts[] = $this->access->getFilterForUserCount();
583
-		if ($search !== '') {
584
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
585
-		}
586
-		$filterParts[] = 'primaryGroupID=' . $groupID;
587
-
588
-		return $this->access->combineFilterWithAnd($filterParts);
589
-	}
590
-
591
-	/**
592
-	 * @throws ServerNotAvailableException
593
-	 */
594
-	public function getUsersInPrimaryGroup(
595
-		string $groupDN,
596
-		string $search = '',
597
-		?int $limit = -1,
598
-		?int $offset = 0
599
-	): array {
600
-		try {
601
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
602
-			$users = $this->access->fetchListOfUsers(
603
-				$filter,
604
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
605
-				$limit,
606
-				$offset
607
-			);
608
-			return $this->access->nextcloudUserNames($users);
609
-		} catch (ServerNotAvailableException $e) {
610
-			throw $e;
611
-		} catch (Exception $e) {
612
-			return [];
613
-		}
614
-	}
615
-
616
-	/**
617
-	 * @throws ServerNotAvailableException
618
-	 */
619
-	public function countUsersInPrimaryGroup(
620
-		string $groupDN,
621
-		string $search = '',
622
-		int $limit = -1,
623
-		int $offset = 0
624
-	): int {
625
-		try {
626
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
627
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
628
-			return (int)$users;
629
-		} catch (ServerNotAvailableException $e) {
630
-			throw $e;
631
-		} catch (Exception $e) {
632
-			return 0;
633
-		}
634
-	}
635
-
636
-	/**
637
-	 * @return string|bool
638
-	 * @throws ServerNotAvailableException
639
-	 */
640
-	public function getUserPrimaryGroup(string $dn) {
641
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
642
-		if ($groupID !== false) {
643
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
644
-			if ($groupName !== false) {
645
-				return $groupName;
646
-			}
647
-		}
648
-
649
-		return false;
650
-	}
651
-
652
-	/**
653
-	 * This function fetches all groups a user belongs to. It does not check
654
-	 * if the user exists at all.
655
-	 *
656
-	 * This function includes groups based on dynamic group membership.
657
-	 *
658
-	 * @param string $uid Name of the user
659
-	 * @return array with group names
660
-	 * @throws Exception
661
-	 * @throws ServerNotAvailableException
662
-	 */
663
-	public function getUserGroups($uid) {
664
-		if (!$this->enabled) {
665
-			return [];
666
-		}
667
-		$cacheKey = 'getUserGroups' . $uid;
668
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
669
-		if (!is_null($userGroups)) {
670
-			return $userGroups;
671
-		}
672
-		$userDN = $this->access->username2dn($uid);
673
-		if (!$userDN) {
674
-			$this->access->connection->writeToCache($cacheKey, []);
675
-			return [];
676
-		}
677
-
678
-		$groups = [];
679
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
680
-		$gidGroupName = $this->getUserGroupByGid($userDN);
681
-
682
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
683
-
684
-		if (!empty($dynamicGroupMemberURL)) {
685
-			// look through dynamic groups to add them to the result array if needed
686
-			$groupsToMatch = $this->access->fetchListOfGroups(
687
-				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
688
-			foreach ($groupsToMatch as $dynamicGroup) {
689
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
690
-					continue;
691
-				}
692
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
693
-				if ($pos !== false) {
694
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
695
-					// apply filter via ldap search to see if this user is in this
696
-					// dynamic group
697
-					$userMatch = $this->access->readAttribute(
698
-						$userDN,
699
-						$this->access->connection->ldapUserDisplayName,
700
-						$memberUrlFilter
701
-					);
702
-					if ($userMatch !== false) {
703
-						// match found so this user is in this group
704
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
705
-						if (is_string($groupName)) {
706
-							// be sure to never return false if the dn could not be
707
-							// resolved to a name, for whatever reason.
708
-							$groups[] = $groupName;
709
-						}
710
-					}
711
-				} else {
712
-					$this->logger->debug('No search filter found on member url of group {dn}',
713
-						[
714
-							'app' => 'user_ldap',
715
-							'dn' => $dynamicGroup,
716
-						]
717
-					);
718
-				}
719
-			}
720
-		}
721
-
722
-		// if possible, read out membership via memberOf. It's far faster than
723
-		// performing a search, which still is a fallback later.
724
-		// memberof doesn't support memberuid, so skip it here.
725
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
726
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
727
-			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
728
-			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
729
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
730
-			if (is_array($groupDNs)) {
731
-				foreach ($groupDNs as $dn) {
732
-					$groupName = $this->access->dn2groupname($dn);
733
-					if (is_string($groupName)) {
734
-						// be sure to never return false if the dn could not be
735
-						// resolved to a name, for whatever reason.
736
-						$groups[] = $groupName;
737
-					}
738
-				}
739
-			}
740
-
741
-			if ($primaryGroup !== false) {
742
-				$groups[] = $primaryGroup;
743
-			}
744
-			if ($gidGroupName !== false) {
745
-				$groups[] = $gidGroupName;
746
-			}
747
-			$this->access->connection->writeToCache($cacheKey, $groups);
748
-			return $groups;
749
-		}
750
-
751
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
752
-		switch ($this->ldapGroupMemberAssocAttr) {
753
-			case 'uniquemember':
754
-			case 'member':
755
-				$uid = $userDN;
756
-				break;
757
-
758
-			case 'memberuid':
759
-			case 'zimbramailforwardingaddress':
760
-				$result = $this->access->readAttribute($userDN, 'uid');
761
-				if ($result === false) {
762
-					$this->logger->debug('No uid attribute found for DN {dn} on {host}',
763
-						[
764
-							'app' => 'user_ldap',
765
-							'dn' => $userDN,
766
-							'host' => $this->access->connection->ldapHost,
767
-						]
768
-					);
769
-					$uid = false;
770
-				} else {
771
-					$uid = $result[0];
772
-				}
773
-				break;
774
-
775
-			default:
776
-				// just in case
777
-				$uid = $userDN;
778
-				break;
779
-		}
780
-
781
-		if ($uid !== false) {
782
-			if (isset($this->cachedGroupsByMember[$uid])) {
783
-				$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
784
-			} else {
785
-				$groupsByMember = array_values($this->getGroupsByMember($uid));
786
-				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
787
-				$this->cachedGroupsByMember[$uid] = $groupsByMember;
788
-				$groups = array_merge($groups, $groupsByMember);
789
-			}
790
-		}
791
-
792
-		if ($primaryGroup !== false) {
793
-			$groups[] = $primaryGroup;
794
-		}
795
-		if ($gidGroupName !== false) {
796
-			$groups[] = $gidGroupName;
797
-		}
798
-
799
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
800
-		$this->access->connection->writeToCache($cacheKey, $groups);
801
-
802
-		return $groups;
803
-	}
804
-
805
-	/**
806
-	 * @throws ServerNotAvailableException
807
-	 */
808
-	private function getGroupsByMember(string $dn, array &$seen = null): array {
809
-		if ($seen === null) {
810
-			$seen = [];
811
-		}
812
-		if (array_key_exists($dn, $seen)) {
813
-			// avoid loops
814
-			return [];
815
-		}
816
-		$allGroups = [];
817
-		$seen[$dn] = true;
818
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
819
-
820
-		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
821
-			//in this case the member entries are email addresses
822
-			$filter .= '@*';
823
-		}
824
-
825
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
826
-		if ($nesting === 0) {
827
-			$filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
828
-		}
829
-
830
-		$groups = $this->access->fetchListOfGroups($filter,
831
-			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
832
-		if (is_array($groups)) {
833
-			$fetcher = function ($dn, &$seen) {
834
-				if (is_array($dn) && isset($dn['dn'][0])) {
835
-					$dn = $dn['dn'][0];
836
-				}
837
-				return $this->getGroupsByMember($dn, $seen);
838
-			};
839
-
840
-			if (empty($dn)) {
841
-				$dn = "";
842
-			}
843
-
844
-			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
845
-		}
846
-		$visibleGroups = $this->filterValidGroups($allGroups);
847
-		return array_intersect_key($allGroups, $visibleGroups);
848
-	}
849
-
850
-	/**
851
-	 * get a list of all users in a group
852
-	 *
853
-	 * @param string $gid
854
-	 * @param string $search
855
-	 * @param int $limit
856
-	 * @param int $offset
857
-	 * @return array with user ids
858
-	 * @throws Exception
859
-	 * @throws ServerNotAvailableException
860
-	 */
861
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
862
-		if (!$this->enabled) {
863
-			return [];
864
-		}
865
-		if (!$this->groupExists($gid)) {
866
-			return [];
867
-		}
868
-		$search = $this->access->escapeFilterPart($search, true);
869
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
870
-		// check for cache of the exact query
871
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
872
-		if (!is_null($groupUsers)) {
873
-			return $groupUsers;
874
-		}
875
-
876
-		if ($limit === -1) {
877
-			$limit = null;
878
-		}
879
-		// check for cache of the query without limit and offset
880
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
881
-		if (!is_null($groupUsers)) {
882
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
883
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
884
-			return $groupUsers;
885
-		}
886
-
887
-		$groupDN = $this->access->groupname2dn($gid);
888
-		if (!$groupDN) {
889
-			// group couldn't be found, return empty resultset
890
-			$this->access->connection->writeToCache($cacheKey, []);
891
-			return [];
892
-		}
893
-
894
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
895
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
896
-		$members = $this->_groupMembers($groupDN);
897
-		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
898
-			//in case users could not be retrieved, return empty result set
899
-			$this->access->connection->writeToCache($cacheKey, []);
900
-			return [];
901
-		}
902
-
903
-		$groupUsers = [];
904
-		$attrs = $this->access->userManager->getAttributes(true);
905
-		foreach ($members as $member) {
906
-			switch ($this->ldapGroupMemberAssocAttr) {
907
-				case 'zimbramailforwardingaddress':
908
-					//we get email addresses and need to convert them to uids
909
-					$parts = explode('@', $member);
910
-					$member = $parts[0];
911
-					//no break needed because we just needed to remove the email part and now we have uids
912
-				case 'memberuid':
913
-					//we got uids, need to get their DNs to 'translate' them to user names
914
-					$filter = $this->access->combineFilterWithAnd([
915
-						str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
916
-						$this->access->combineFilterWithAnd([
917
-							$this->access->getFilterPartForUserSearch($search),
918
-							$this->access->connection->ldapUserFilter
919
-						])
920
-					]);
921
-					$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
922
-					if (empty($ldap_users)) {
923
-						break;
924
-					}
925
-					$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
926
-					break;
927
-				default:
928
-					//we got DNs, check if we need to filter by search or we can give back all of them
929
-					$uid = $this->access->dn2username($member);
930
-					if (!$uid) {
931
-						break;
932
-					}
933
-
934
-					$cacheKey = 'userExistsOnLDAP' . $uid;
935
-					$userExists = $this->access->connection->getFromCache($cacheKey);
936
-					if ($userExists === false) {
937
-						break;
938
-					}
939
-					if ($userExists === null || $search !== '') {
940
-						if (!$this->access->readAttribute($member,
941
-							$this->access->connection->ldapUserDisplayName,
942
-							$this->access->combineFilterWithAnd([
943
-								$this->access->getFilterPartForUserSearch($search),
944
-								$this->access->connection->ldapUserFilter
945
-							]))) {
946
-							if ($search === '') {
947
-								$this->access->connection->writeToCache($cacheKey, false);
948
-							}
949
-							break;
950
-						}
951
-						$this->access->connection->writeToCache($cacheKey, true);
952
-					}
953
-					$groupUsers[] = $uid;
954
-					break;
955
-			}
956
-		}
957
-
958
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
959
-		natsort($groupUsers);
960
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
961
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
962
-
963
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
964
-
965
-		return $groupUsers;
966
-	}
967
-
968
-	/**
969
-	 * returns the number of users in a group, who match the search term
970
-	 *
971
-	 * @param string $gid the internal group name
972
-	 * @param string $search optional, a search string
973
-	 * @return int|bool
974
-	 * @throws Exception
975
-	 * @throws ServerNotAvailableException
976
-	 */
977
-	public function countUsersInGroup($gid, $search = '') {
978
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
979
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
980
-		}
981
-
982
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
983
-		if (!$this->enabled || !$this->groupExists($gid)) {
984
-			return false;
985
-		}
986
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
987
-		if (!is_null($groupUsers)) {
988
-			return $groupUsers;
989
-		}
990
-
991
-		$groupDN = $this->access->groupname2dn($gid);
992
-		if (!$groupDN) {
993
-			// group couldn't be found, return empty result set
994
-			$this->access->connection->writeToCache($cacheKey, false);
995
-			return false;
996
-		}
997
-
998
-		$members = $this->_groupMembers($groupDN);
999
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
1000
-		if (!$members && $primaryUserCount === 0) {
1001
-			//in case users could not be retrieved, return empty result set
1002
-			$this->access->connection->writeToCache($cacheKey, false);
1003
-			return false;
1004
-		}
1005
-
1006
-		if ($search === '') {
1007
-			$groupUsers = count($members) + $primaryUserCount;
1008
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
1009
-			return $groupUsers;
1010
-		}
1011
-		$search = $this->access->escapeFilterPart($search, true);
1012
-		$isMemberUid =
1013
-			($this->ldapGroupMemberAssocAttr === 'memberuid' ||
1014
-				$this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
1015
-
1016
-		//we need to apply the search filter
1017
-		//alternatives that need to be checked:
1018
-		//a) get all users by search filter and array_intersect them
1019
-		//b) a, but only when less than 1k 10k ?k users like it is
1020
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
1021
-		//   and let it count.
1022
-		//For now this is not important, because the only use of this method
1023
-		//does not supply a search string
1024
-		$groupUsers = [];
1025
-		foreach ($members as $member) {
1026
-			if ($isMemberUid) {
1027
-				if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
1028
-					//we get email addresses and need to convert them to uids
1029
-					$parts = explode('@', $member);
1030
-					$member = $parts[0];
1031
-				}
1032
-				//we got uids, need to get their DNs to 'translate' them to user names
1033
-				$filter = $this->access->combineFilterWithAnd([
1034
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
1035
-					$this->access->getFilterPartForUserSearch($search)
1036
-				]);
1037
-				$ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
1038
-				if (count($ldap_users) < 1) {
1039
-					continue;
1040
-				}
1041
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
1042
-			} else {
1043
-				//we need to apply the search filter now
1044
-				if (!$this->access->readAttribute($member,
1045
-					$this->access->connection->ldapUserDisplayName,
1046
-					$this->access->getFilterPartForUserSearch($search))) {
1047
-					continue;
1048
-				}
1049
-				// dn2username will also check if the users belong to the allowed base
1050
-				if ($ncGroupId = $this->access->dn2username($member)) {
1051
-					$groupUsers[] = $ncGroupId;
1052
-				}
1053
-			}
1054
-		}
1055
-
1056
-		//and get users that have the group as primary
1057
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1058
-
1059
-		return count($groupUsers) + $primaryUsers;
1060
-	}
1061
-
1062
-	/**
1063
-	 * get a list of all groups using a paged search
1064
-	 *
1065
-	 * @param string $search
1066
-	 * @param int $limit
1067
-	 * @param int $offset
1068
-	 * @return array with group names
1069
-	 *
1070
-	 * Returns a list with all groups
1071
-	 * Uses a paged search if available to override a
1072
-	 * server side search limit.
1073
-	 * (active directory has a limit of 1000 by default)
1074
-	 * @throws Exception
1075
-	 */
1076
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1077
-		if (!$this->enabled) {
1078
-			return [];
1079
-		}
1080
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1081
-
1082
-		//Check cache before driving unnecessary searches
1083
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
1084
-		if (!is_null($ldap_groups)) {
1085
-			return $ldap_groups;
1086
-		}
1087
-
1088
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1089
-		// error. With a limit of 0, we get 0 results. So we pass null.
1090
-		if ($limit <= 0) {
1091
-			$limit = null;
1092
-		}
1093
-		$filter = $this->access->combineFilterWithAnd([
1094
-			$this->access->connection->ldapGroupFilter,
1095
-			$this->access->getFilterPartForGroupSearch($search)
1096
-		]);
1097
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
1098
-			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1099
-			$limit,
1100
-			$offset);
1101
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1102
-
1103
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1104
-		return $ldap_groups;
1105
-	}
1106
-
1107
-	/**
1108
-	 * check if a group exists
1109
-	 *
1110
-	 * @param string $gid
1111
-	 * @return bool
1112
-	 * @throws ServerNotAvailableException
1113
-	 */
1114
-	public function groupExists($gid) {
1115
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1116
-		if (!is_null($groupExists)) {
1117
-			return (bool)$groupExists;
1118
-		}
1119
-
1120
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1121
-		//only, requires more checking.
1122
-		$dn = $this->access->groupname2dn($gid);
1123
-		if (!$dn) {
1124
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1125
-			return false;
1126
-		}
1127
-
1128
-		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1129
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1130
-			return false;
1131
-		}
1132
-
1133
-		//if group really still exists, we will be able to read its objectClass
1134
-		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1135
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1136
-			return false;
1137
-		}
1138
-
1139
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1140
-		return true;
1141
-	}
1142
-
1143
-	/**
1144
-	 * @throws ServerNotAvailableException
1145
-	 * @throws Exception
1146
-	 */
1147
-	protected function filterValidGroups(array $listOfGroups): array {
1148
-		$validGroupDNs = [];
1149
-		foreach ($listOfGroups as $key => $item) {
1150
-			$dn = is_string($item) ? $item : $item['dn'][0];
1151
-			$gid = $this->access->dn2groupname($dn);
1152
-			if (!$gid) {
1153
-				continue;
1154
-			}
1155
-			if ($this->groupExists($gid)) {
1156
-				$validGroupDNs[$key] = $item;
1157
-			}
1158
-		}
1159
-		return $validGroupDNs;
1160
-	}
1161
-
1162
-	/**
1163
-	 * Check if backend implements actions
1164
-	 *
1165
-	 * @param int $actions bitwise-or'ed actions
1166
-	 * @return boolean
1167
-	 *
1168
-	 * Returns the supported actions as int to be
1169
-	 * compared with GroupInterface::CREATE_GROUP etc.
1170
-	 */
1171
-	public function implementsActions($actions) {
1172
-		return (bool)((GroupInterface::COUNT_USERS |
1173
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1174
-	}
1175
-
1176
-	/**
1177
-	 * Return access for LDAP interaction.
1178
-	 *
1179
-	 * @return Access instance of Access for LDAP interaction
1180
-	 */
1181
-	public function getLDAPAccess($gid) {
1182
-		return $this->access;
1183
-	}
1184
-
1185
-	/**
1186
-	 * create a group
1187
-	 *
1188
-	 * @param string $gid
1189
-	 * @return bool
1190
-	 * @throws Exception
1191
-	 * @throws ServerNotAvailableException
1192
-	 */
1193
-	public function createGroup($gid) {
1194
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1195
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1196
-				//updates group mapping
1197
-				$uuid = $this->access->getUUID($dn, false);
1198
-				if (is_string($uuid)) {
1199
-					$this->access->mapAndAnnounceIfApplicable(
1200
-						$this->access->getGroupMapper(),
1201
-						$dn,
1202
-						$gid,
1203
-						$uuid,
1204
-						false
1205
-					);
1206
-					$this->access->cacheGroupExists($gid);
1207
-				}
1208
-			}
1209
-			return $dn != null;
1210
-		}
1211
-		throw new Exception('Could not create group in LDAP backend.');
1212
-	}
1213
-
1214
-	/**
1215
-	 * delete a group
1216
-	 *
1217
-	 * @param string $gid gid of the group to delete
1218
-	 * @return bool
1219
-	 * @throws Exception
1220
-	 */
1221
-	public function deleteGroup($gid) {
1222
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1223
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1224
-				#delete group in nextcloud internal db
1225
-				$this->access->getGroupMapper()->unmap($gid);
1226
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1227
-			}
1228
-			return $ret;
1229
-		}
1230
-		throw new Exception('Could not delete group in LDAP backend.');
1231
-	}
1232
-
1233
-	/**
1234
-	 * Add a user to a group
1235
-	 *
1236
-	 * @param string $uid Name of the user to add to group
1237
-	 * @param string $gid Name of the group in which add the user
1238
-	 * @return bool
1239
-	 * @throws Exception
1240
-	 */
1241
-	public function addToGroup($uid, $gid) {
1242
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1243
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1244
-				$this->access->connection->clearCache();
1245
-				unset($this->cachedGroupMembers[$gid]);
1246
-			}
1247
-			return $ret;
1248
-		}
1249
-		throw new Exception('Could not add user to group in LDAP backend.');
1250
-	}
1251
-
1252
-	/**
1253
-	 * Removes a user from a group
1254
-	 *
1255
-	 * @param string $uid Name of the user to remove from group
1256
-	 * @param string $gid Name of the group from which remove the user
1257
-	 * @return bool
1258
-	 * @throws Exception
1259
-	 */
1260
-	public function removeFromGroup($uid, $gid) {
1261
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1262
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1263
-				$this->access->connection->clearCache();
1264
-				unset($this->cachedGroupMembers[$gid]);
1265
-			}
1266
-			return $ret;
1267
-		}
1268
-		throw new Exception('Could not remove user from group in LDAP backend.');
1269
-	}
1270
-
1271
-	/**
1272
-	 * Gets group details
1273
-	 *
1274
-	 * @param string $gid Name of the group
1275
-	 * @return array|false
1276
-	 * @throws Exception
1277
-	 */
1278
-	public function getGroupDetails($gid) {
1279
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1280
-			return $this->groupPluginManager->getGroupDetails($gid);
1281
-		}
1282
-		throw new Exception('Could not get group details in LDAP backend.');
1283
-	}
1284
-
1285
-	/**
1286
-	 * Return LDAP connection resource from a cloned connection.
1287
-	 * The cloned connection needs to be closed manually.
1288
-	 * of the current access.
1289
-	 *
1290
-	 * @param string $gid
1291
-	 * @return resource of the LDAP connection
1292
-	 * @throws ServerNotAvailableException
1293
-	 */
1294
-	public function getNewLDAPConnection($gid) {
1295
-		$connection = clone $this->access->getConnection();
1296
-		return $connection->getConnectionResource();
1297
-	}
1298
-
1299
-	/**
1300
-	 * @throws ServerNotAvailableException
1301
-	 */
1302
-	public function getDisplayName(string $gid): string {
1303
-		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1304
-			return $this->groupPluginManager->getDisplayName($gid);
1305
-		}
1306
-
1307
-		$cacheKey = 'group_getDisplayName' . $gid;
1308
-		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1309
-			return $displayName;
1310
-		}
1311
-
1312
-		$displayName = $this->access->readAttribute(
1313
-			$this->access->groupname2dn($gid),
1314
-			$this->access->connection->ldapGroupDisplayName);
1315
-
1316
-		if ($displayName && (count($displayName) > 0)) {
1317
-			$displayName = $displayName[0];
1318
-			$this->access->connection->writeToCache($cacheKey, $displayName);
1319
-			return $displayName;
1320
-		}
1321
-
1322
-		return '';
1323
-	}
56
+    protected $enabled = false;
57
+
58
+    /** @var string[] $cachedGroupMembers array of users with gid as key */
59
+    protected $cachedGroupMembers;
60
+    /** @var string[] $cachedGroupsByMember array of groups with uid as key */
61
+    protected $cachedGroupsByMember;
62
+    /** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
63
+    protected $cachedNestedGroups;
64
+    /** @var GroupPluginManager */
65
+    protected $groupPluginManager;
66
+    /** @var ILogger */
67
+    protected $logger;
68
+
69
+    /**
70
+     * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
71
+     */
72
+    protected $ldapGroupMemberAssocAttr;
73
+
74
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
75
+        parent::__construct($access);
76
+        $filter = $this->access->connection->ldapGroupFilter;
77
+        $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
78
+        if (!empty($filter) && !empty($gAssoc)) {
79
+            $this->enabled = true;
80
+        }
81
+
82
+        $this->cachedGroupMembers = new CappedMemoryCache();
83
+        $this->cachedGroupsByMember = new CappedMemoryCache();
84
+        $this->cachedNestedGroups = new CappedMemoryCache();
85
+        $this->groupPluginManager = $groupPluginManager;
86
+        $this->logger = OC::$server->getLogger();
87
+        $this->ldapGroupMemberAssocAttr = strtolower($gAssoc);
88
+    }
89
+
90
+    /**
91
+     * is user in group?
92
+     *
93
+     * @param string $uid uid of the user
94
+     * @param string $gid gid of the group
95
+     * @return bool
96
+     * @throws Exception
97
+     * @throws ServerNotAvailableException
98
+     */
99
+    public function inGroup($uid, $gid) {
100
+        if (!$this->enabled) {
101
+            return false;
102
+        }
103
+        $cacheKey = 'inGroup' . $uid . ':' . $gid;
104
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
105
+        if (!is_null($inGroup)) {
106
+            return (bool)$inGroup;
107
+        }
108
+
109
+        $userDN = $this->access->username2dn($uid);
110
+
111
+        if (isset($this->cachedGroupMembers[$gid])) {
112
+            return in_array($userDN, $this->cachedGroupMembers[$gid]);
113
+        }
114
+
115
+        $cacheKeyMembers = 'inGroup-members:' . $gid;
116
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
117
+        if (!is_null($members)) {
118
+            $this->cachedGroupMembers[$gid] = $members;
119
+            $isInGroup = in_array($userDN, $members, true);
120
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
121
+            return $isInGroup;
122
+        }
123
+
124
+        $groupDN = $this->access->groupname2dn($gid);
125
+        // just in case
126
+        if (!$groupDN || !$userDN) {
127
+            $this->access->connection->writeToCache($cacheKey, false);
128
+            return false;
129
+        }
130
+
131
+        //check primary group first
132
+        if ($gid === $this->getUserPrimaryGroup($userDN)) {
133
+            $this->access->connection->writeToCache($cacheKey, true);
134
+            return true;
135
+        }
136
+
137
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
138
+        $members = $this->_groupMembers($groupDN);
139
+        if (!is_array($members) || count($members) === 0) {
140
+            $this->access->connection->writeToCache($cacheKey, false);
141
+            return false;
142
+        }
143
+
144
+        //extra work if we don't get back user DNs
145
+        switch ($this->ldapGroupMemberAssocAttr) {
146
+            case 'memberuid':
147
+            case 'zimbramailforwardingaddress':
148
+                $requestAttributes = $this->access->userManager->getAttributes(true);
149
+                $dns = [];
150
+                $filterParts = [];
151
+                $bytes = 0;
152
+                foreach ($members as $mid) {
153
+                    if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
154
+                        $parts = explode('@', $mid); //making sure we get only the uid
155
+                        $mid = $parts[0];
156
+                    }
157
+                    $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
158
+                    $filterParts[] = $filter;
159
+                    $bytes += strlen($filter);
160
+                    if ($bytes >= 9000000) {
161
+                        // AD has a default input buffer of 10 MB, we do not want
162
+                        // to take even the chance to exceed it
163
+                        $filter = $this->access->combineFilterWithOr($filterParts);
164
+                        $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
165
+                        $bytes = 0;
166
+                        $filterParts = [];
167
+                        $dns = array_merge($dns, $users);
168
+                    }
169
+                }
170
+                if (count($filterParts) > 0) {
171
+                    $filter = $this->access->combineFilterWithOr($filterParts);
172
+                    $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
173
+                    $dns = array_merge($dns, $users);
174
+                }
175
+                $members = $dns;
176
+                break;
177
+        }
178
+
179
+        $isInGroup = in_array($userDN, $members);
180
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
181
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
182
+        $this->cachedGroupMembers[$gid] = $members;
183
+
184
+        return $isInGroup;
185
+    }
186
+
187
+    /**
188
+     * For a group that has user membership defined by an LDAP search url
189
+     * attribute returns the users that match the search url otherwise returns
190
+     * an empty array.
191
+     *
192
+     * @throws ServerNotAvailableException
193
+     */
194
+    public function getDynamicGroupMembers(string $dnGroup): array {
195
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
196
+
197
+        if (empty($dynamicGroupMemberURL)) {
198
+            return [];
199
+        }
200
+
201
+        $dynamicMembers = [];
202
+        $memberURLs = $this->access->readAttribute(
203
+            $dnGroup,
204
+            $dynamicGroupMemberURL,
205
+            $this->access->connection->ldapGroupFilter
206
+        );
207
+        if ($memberURLs !== false) {
208
+            // this group has the 'memberURL' attribute so this is a dynamic group
209
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
210
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
211
+            $pos = strpos($memberURLs[0], '(');
212
+            if ($pos !== false) {
213
+                $memberUrlFilter = substr($memberURLs[0], $pos);
214
+                $foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
215
+                $dynamicMembers = [];
216
+                foreach ($foundMembers as $value) {
217
+                    $dynamicMembers[$value['dn'][0]] = 1;
218
+                }
219
+            } else {
220
+                $this->logger->debug('No search filter found on member url of group {dn}',
221
+                    [
222
+                        'app' => 'user_ldap',
223
+                        'dn' => $dnGroup,
224
+                    ]
225
+                );
226
+            }
227
+        }
228
+        return $dynamicMembers;
229
+    }
230
+
231
+    /**
232
+     * @throws ServerNotAvailableException
233
+     */
234
+    private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
235
+        if ($seen === null) {
236
+            $seen = [];
237
+        }
238
+        $allMembers = [];
239
+        if (array_key_exists($dnGroup, $seen)) {
240
+            return [];
241
+        }
242
+        // used extensively in cron job, caching makes sense for nested groups
243
+        $cacheKey = '_groupMembers' . $dnGroup;
244
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
245
+        if ($groupMembers !== null) {
246
+            return $groupMembers;
247
+        }
248
+
249
+        if ($this->access->connection->ldapNestedGroups
250
+            && $this->access->connection->useMemberOfToDetectMembership
251
+            && $this->access->connection->hasMemberOfFilterSupport
252
+            && $this->access->connection->ldapMatchingRuleInChainState !== Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE
253
+        ) {
254
+            $attemptedLdapMatchingRuleInChain = true;
255
+            // compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
256
+            $filter = $this->access->combineFilterWithAnd([
257
+                $this->access->connection->ldapUserFilter,
258
+                $this->access->connection->ldapUserDisplayName . '=*',
259
+                'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
260
+            ]);
261
+            $memberRecords = $this->access->fetchListOfUsers(
262
+                $filter,
263
+                $this->access->userManager->getAttributes(true)
264
+            );
265
+            $result = array_reduce($memberRecords, function ($carry, $record) {
266
+                $carry[] = $record['dn'][0];
267
+                return $carry;
268
+            }, []);
269
+            if ($this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_AVAILABLE) {
270
+                return $result;
271
+            } elseif (!empty($memberRecords)) {
272
+                $this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_AVAILABLE;
273
+                $this->access->connection->saveConfiguration();
274
+                return $result;
275
+            }
276
+            // when feature availability is unknown, and the result is empty, continue and test with original approach
277
+        }
278
+
279
+        $seen[$dnGroup] = 1;
280
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
281
+        if (is_array($members)) {
282
+            $fetcher = function ($memberDN, &$seen) {
283
+                return $this->_groupMembers($memberDN, $seen);
284
+            };
285
+            $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
286
+        }
287
+
288
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
289
+
290
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
291
+        if (isset($attemptedLdapMatchingRuleInChain)
292
+            && $this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_UNKNOWN
293
+            && !empty($allMembers)
294
+        ) {
295
+            $this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE;
296
+            $this->access->connection->saveConfiguration();
297
+        }
298
+        return $allMembers;
299
+    }
300
+
301
+    /**
302
+     * @throws ServerNotAvailableException
303
+     */
304
+    private function _getGroupDNsFromMemberOf(string $dn): array {
305
+        $groups = $this->access->readAttribute($dn, 'memberOf');
306
+        if (!is_array($groups)) {
307
+            return [];
308
+        }
309
+
310
+        $fetcher = function ($groupDN) {
311
+            if (isset($this->cachedNestedGroups[$groupDN])) {
312
+                $nestedGroups = $this->cachedNestedGroups[$groupDN];
313
+            } else {
314
+                $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
315
+                if (!is_array($nestedGroups)) {
316
+                    $nestedGroups = [];
317
+                }
318
+                $this->cachedNestedGroups[$groupDN] = $nestedGroups;
319
+            }
320
+            return $nestedGroups;
321
+        };
322
+
323
+        $groups = $this->walkNestedGroups($dn, $fetcher, $groups);
324
+        return $this->filterValidGroups($groups);
325
+    }
326
+
327
+    private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
328
+        $nesting = (int)$this->access->connection->ldapNestedGroups;
329
+        // depending on the input, we either have a list of DNs or a list of LDAP records
330
+        // also, the output expects either DNs or records. Testing the first element should suffice.
331
+        $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
332
+
333
+        if ($nesting !== 1) {
334
+            if ($recordMode) {
335
+                // the keys are numeric, but should hold the DN
336
+                return array_reduce($list, function ($transformed, $record) use ($dn) {
337
+                    if ($record['dn'][0] != $dn) {
338
+                        $transformed[$record['dn'][0]] = $record;
339
+                    }
340
+                    return $transformed;
341
+                }, []);
342
+            }
343
+            return $list;
344
+        }
345
+
346
+        $seen = [];
347
+        while ($record = array_shift($list)) {
348
+            $recordDN = $recordMode ? $record['dn'][0] : $record;
349
+            if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
350
+                // Prevent loops
351
+                continue;
352
+            }
353
+            $fetched = $fetcher($record, $seen);
354
+            $list = array_merge($list, $fetched);
355
+            $seen[$recordDN] = $record;
356
+        }
357
+
358
+        return $recordMode ? $seen : array_keys($seen);
359
+    }
360
+
361
+    /**
362
+     * translates a gidNumber into an ownCloud internal name
363
+     *
364
+     * @return string|bool
365
+     * @throws Exception
366
+     * @throws ServerNotAvailableException
367
+     */
368
+    public function gidNumber2Name(string $gid, string $dn) {
369
+        $cacheKey = 'gidNumberToName' . $gid;
370
+        $groupName = $this->access->connection->getFromCache($cacheKey);
371
+        if (!is_null($groupName) && isset($groupName)) {
372
+            return $groupName;
373
+        }
374
+
375
+        //we need to get the DN from LDAP
376
+        $filter = $this->access->combineFilterWithAnd([
377
+            $this->access->connection->ldapGroupFilter,
378
+            'objectClass=posixGroup',
379
+            $this->access->connection->ldapGidNumber . '=' . $gid
380
+        ]);
381
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
382
+    }
383
+
384
+    /**
385
+     * @throws ServerNotAvailableException
386
+     * @throws Exception
387
+     */
388
+    private function getNameOfGroup(string $filter, string $cacheKey) {
389
+        $result = $this->access->searchGroups($filter, ['dn'], 1);
390
+        if (empty($result)) {
391
+            return null;
392
+        }
393
+        $dn = $result[0]['dn'][0];
394
+
395
+        //and now the group name
396
+        //NOTE once we have separate Nextcloud group IDs and group names we can
397
+        //directly read the display name attribute instead of the DN
398
+        $name = $this->access->dn2groupname($dn);
399
+
400
+        $this->access->connection->writeToCache($cacheKey, $name);
401
+
402
+        return $name;
403
+    }
404
+
405
+    /**
406
+     * returns the entry's gidNumber
407
+     *
408
+     * @return string|bool
409
+     * @throws ServerNotAvailableException
410
+     */
411
+    private function getEntryGidNumber(string $dn, string $attribute) {
412
+        $value = $this->access->readAttribute($dn, $attribute);
413
+        if (is_array($value) && !empty($value)) {
414
+            return $value[0];
415
+        }
416
+        return false;
417
+    }
418
+
419
+    /**
420
+     * @return string|bool
421
+     * @throws ServerNotAvailableException
422
+     */
423
+    public function getGroupGidNumber(string $dn) {
424
+        return $this->getEntryGidNumber($dn, 'gidNumber');
425
+    }
426
+
427
+    /**
428
+     * returns the user's gidNumber
429
+     *
430
+     * @return string|bool
431
+     * @throws ServerNotAvailableException
432
+     */
433
+    public function getUserGidNumber(string $dn) {
434
+        $gidNumber = false;
435
+        if ($this->access->connection->hasGidNumber) {
436
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
437
+            if ($gidNumber === false) {
438
+                $this->access->connection->hasGidNumber = false;
439
+            }
440
+        }
441
+        return $gidNumber;
442
+    }
443
+
444
+    /**
445
+     * @throws ServerNotAvailableException
446
+     * @throws Exception
447
+     */
448
+    private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
449
+        $groupID = $this->getGroupGidNumber($groupDN);
450
+        if ($groupID === false) {
451
+            throw new Exception('Not a valid group');
452
+        }
453
+
454
+        $filterParts = [];
455
+        $filterParts[] = $this->access->getFilterForUserCount();
456
+        if ($search !== '') {
457
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
458
+        }
459
+        $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
460
+
461
+        return $this->access->combineFilterWithAnd($filterParts);
462
+    }
463
+
464
+    /**
465
+     * returns a list of users that have the given group as gid number
466
+     *
467
+     * @throws ServerNotAvailableException
468
+     */
469
+    public function getUsersInGidNumber(
470
+        string $groupDN,
471
+        string $search = '',
472
+        ?int $limit = -1,
473
+        ?int $offset = 0
474
+    ): array {
475
+        try {
476
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
477
+            $users = $this->access->fetchListOfUsers(
478
+                $filter,
479
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
480
+                $limit,
481
+                $offset
482
+            );
483
+            return $this->access->nextcloudUserNames($users);
484
+        } catch (ServerNotAvailableException $e) {
485
+            throw $e;
486
+        } catch (Exception $e) {
487
+            return [];
488
+        }
489
+    }
490
+
491
+    /**
492
+     * @throws ServerNotAvailableException
493
+     * @return bool
494
+     */
495
+    public function getUserGroupByGid(string $dn) {
496
+        $groupID = $this->getUserGidNumber($dn);
497
+        if ($groupID !== false) {
498
+            $groupName = $this->gidNumber2Name($groupID, $dn);
499
+            if ($groupName !== false) {
500
+                return $groupName;
501
+            }
502
+        }
503
+
504
+        return false;
505
+    }
506
+
507
+    /**
508
+     * translates a primary group ID into an Nextcloud internal name
509
+     *
510
+     * @return string|bool
511
+     * @throws Exception
512
+     * @throws ServerNotAvailableException
513
+     */
514
+    public function primaryGroupID2Name(string $gid, string $dn) {
515
+        $cacheKey = 'primaryGroupIDtoName';
516
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
517
+        if (!is_null($groupNames) && isset($groupNames[$gid])) {
518
+            return $groupNames[$gid];
519
+        }
520
+
521
+        $domainObjectSid = $this->access->getSID($dn);
522
+        if ($domainObjectSid === false) {
523
+            return false;
524
+        }
525
+
526
+        //we need to get the DN from LDAP
527
+        $filter = $this->access->combineFilterWithAnd([
528
+            $this->access->connection->ldapGroupFilter,
529
+            'objectsid=' . $domainObjectSid . '-' . $gid
530
+        ]);
531
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
532
+    }
533
+
534
+    /**
535
+     * returns the entry's primary group ID
536
+     *
537
+     * @return string|bool
538
+     * @throws ServerNotAvailableException
539
+     */
540
+    private function getEntryGroupID(string $dn, string $attribute) {
541
+        $value = $this->access->readAttribute($dn, $attribute);
542
+        if (is_array($value) && !empty($value)) {
543
+            return $value[0];
544
+        }
545
+        return false;
546
+    }
547
+
548
+    /**
549
+     * @return string|bool
550
+     * @throws ServerNotAvailableException
551
+     */
552
+    public function getGroupPrimaryGroupID(string $dn) {
553
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
554
+    }
555
+
556
+    /**
557
+     * @return string|bool
558
+     * @throws ServerNotAvailableException
559
+     */
560
+    public function getUserPrimaryGroupIDs(string $dn) {
561
+        $primaryGroupID = false;
562
+        if ($this->access->connection->hasPrimaryGroups) {
563
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
564
+            if ($primaryGroupID === false) {
565
+                $this->access->connection->hasPrimaryGroups = false;
566
+            }
567
+        }
568
+        return $primaryGroupID;
569
+    }
570
+
571
+    /**
572
+     * @throws Exception
573
+     * @throws ServerNotAvailableException
574
+     */
575
+    private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
576
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
577
+        if ($groupID === false) {
578
+            throw new Exception('Not a valid group');
579
+        }
580
+
581
+        $filterParts = [];
582
+        $filterParts[] = $this->access->getFilterForUserCount();
583
+        if ($search !== '') {
584
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
585
+        }
586
+        $filterParts[] = 'primaryGroupID=' . $groupID;
587
+
588
+        return $this->access->combineFilterWithAnd($filterParts);
589
+    }
590
+
591
+    /**
592
+     * @throws ServerNotAvailableException
593
+     */
594
+    public function getUsersInPrimaryGroup(
595
+        string $groupDN,
596
+        string $search = '',
597
+        ?int $limit = -1,
598
+        ?int $offset = 0
599
+    ): array {
600
+        try {
601
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
602
+            $users = $this->access->fetchListOfUsers(
603
+                $filter,
604
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
605
+                $limit,
606
+                $offset
607
+            );
608
+            return $this->access->nextcloudUserNames($users);
609
+        } catch (ServerNotAvailableException $e) {
610
+            throw $e;
611
+        } catch (Exception $e) {
612
+            return [];
613
+        }
614
+    }
615
+
616
+    /**
617
+     * @throws ServerNotAvailableException
618
+     */
619
+    public function countUsersInPrimaryGroup(
620
+        string $groupDN,
621
+        string $search = '',
622
+        int $limit = -1,
623
+        int $offset = 0
624
+    ): int {
625
+        try {
626
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
627
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
628
+            return (int)$users;
629
+        } catch (ServerNotAvailableException $e) {
630
+            throw $e;
631
+        } catch (Exception $e) {
632
+            return 0;
633
+        }
634
+    }
635
+
636
+    /**
637
+     * @return string|bool
638
+     * @throws ServerNotAvailableException
639
+     */
640
+    public function getUserPrimaryGroup(string $dn) {
641
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
642
+        if ($groupID !== false) {
643
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
644
+            if ($groupName !== false) {
645
+                return $groupName;
646
+            }
647
+        }
648
+
649
+        return false;
650
+    }
651
+
652
+    /**
653
+     * This function fetches all groups a user belongs to. It does not check
654
+     * if the user exists at all.
655
+     *
656
+     * This function includes groups based on dynamic group membership.
657
+     *
658
+     * @param string $uid Name of the user
659
+     * @return array with group names
660
+     * @throws Exception
661
+     * @throws ServerNotAvailableException
662
+     */
663
+    public function getUserGroups($uid) {
664
+        if (!$this->enabled) {
665
+            return [];
666
+        }
667
+        $cacheKey = 'getUserGroups' . $uid;
668
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
669
+        if (!is_null($userGroups)) {
670
+            return $userGroups;
671
+        }
672
+        $userDN = $this->access->username2dn($uid);
673
+        if (!$userDN) {
674
+            $this->access->connection->writeToCache($cacheKey, []);
675
+            return [];
676
+        }
677
+
678
+        $groups = [];
679
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
680
+        $gidGroupName = $this->getUserGroupByGid($userDN);
681
+
682
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
683
+
684
+        if (!empty($dynamicGroupMemberURL)) {
685
+            // look through dynamic groups to add them to the result array if needed
686
+            $groupsToMatch = $this->access->fetchListOfGroups(
687
+                $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
688
+            foreach ($groupsToMatch as $dynamicGroup) {
689
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
690
+                    continue;
691
+                }
692
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
693
+                if ($pos !== false) {
694
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
695
+                    // apply filter via ldap search to see if this user is in this
696
+                    // dynamic group
697
+                    $userMatch = $this->access->readAttribute(
698
+                        $userDN,
699
+                        $this->access->connection->ldapUserDisplayName,
700
+                        $memberUrlFilter
701
+                    );
702
+                    if ($userMatch !== false) {
703
+                        // match found so this user is in this group
704
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
705
+                        if (is_string($groupName)) {
706
+                            // be sure to never return false if the dn could not be
707
+                            // resolved to a name, for whatever reason.
708
+                            $groups[] = $groupName;
709
+                        }
710
+                    }
711
+                } else {
712
+                    $this->logger->debug('No search filter found on member url of group {dn}',
713
+                        [
714
+                            'app' => 'user_ldap',
715
+                            'dn' => $dynamicGroup,
716
+                        ]
717
+                    );
718
+                }
719
+            }
720
+        }
721
+
722
+        // if possible, read out membership via memberOf. It's far faster than
723
+        // performing a search, which still is a fallback later.
724
+        // memberof doesn't support memberuid, so skip it here.
725
+        if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
726
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
727
+            && $this->ldapGroupMemberAssocAttr !== 'memberuid'
728
+            && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
729
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
730
+            if (is_array($groupDNs)) {
731
+                foreach ($groupDNs as $dn) {
732
+                    $groupName = $this->access->dn2groupname($dn);
733
+                    if (is_string($groupName)) {
734
+                        // be sure to never return false if the dn could not be
735
+                        // resolved to a name, for whatever reason.
736
+                        $groups[] = $groupName;
737
+                    }
738
+                }
739
+            }
740
+
741
+            if ($primaryGroup !== false) {
742
+                $groups[] = $primaryGroup;
743
+            }
744
+            if ($gidGroupName !== false) {
745
+                $groups[] = $gidGroupName;
746
+            }
747
+            $this->access->connection->writeToCache($cacheKey, $groups);
748
+            return $groups;
749
+        }
750
+
751
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
752
+        switch ($this->ldapGroupMemberAssocAttr) {
753
+            case 'uniquemember':
754
+            case 'member':
755
+                $uid = $userDN;
756
+                break;
757
+
758
+            case 'memberuid':
759
+            case 'zimbramailforwardingaddress':
760
+                $result = $this->access->readAttribute($userDN, 'uid');
761
+                if ($result === false) {
762
+                    $this->logger->debug('No uid attribute found for DN {dn} on {host}',
763
+                        [
764
+                            'app' => 'user_ldap',
765
+                            'dn' => $userDN,
766
+                            'host' => $this->access->connection->ldapHost,
767
+                        ]
768
+                    );
769
+                    $uid = false;
770
+                } else {
771
+                    $uid = $result[0];
772
+                }
773
+                break;
774
+
775
+            default:
776
+                // just in case
777
+                $uid = $userDN;
778
+                break;
779
+        }
780
+
781
+        if ($uid !== false) {
782
+            if (isset($this->cachedGroupsByMember[$uid])) {
783
+                $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
784
+            } else {
785
+                $groupsByMember = array_values($this->getGroupsByMember($uid));
786
+                $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
787
+                $this->cachedGroupsByMember[$uid] = $groupsByMember;
788
+                $groups = array_merge($groups, $groupsByMember);
789
+            }
790
+        }
791
+
792
+        if ($primaryGroup !== false) {
793
+            $groups[] = $primaryGroup;
794
+        }
795
+        if ($gidGroupName !== false) {
796
+            $groups[] = $gidGroupName;
797
+        }
798
+
799
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
800
+        $this->access->connection->writeToCache($cacheKey, $groups);
801
+
802
+        return $groups;
803
+    }
804
+
805
+    /**
806
+     * @throws ServerNotAvailableException
807
+     */
808
+    private function getGroupsByMember(string $dn, array &$seen = null): array {
809
+        if ($seen === null) {
810
+            $seen = [];
811
+        }
812
+        if (array_key_exists($dn, $seen)) {
813
+            // avoid loops
814
+            return [];
815
+        }
816
+        $allGroups = [];
817
+        $seen[$dn] = true;
818
+        $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
819
+
820
+        if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
821
+            //in this case the member entries are email addresses
822
+            $filter .= '@*';
823
+        }
824
+
825
+        $nesting = (int)$this->access->connection->ldapNestedGroups;
826
+        if ($nesting === 0) {
827
+            $filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
828
+        }
829
+
830
+        $groups = $this->access->fetchListOfGroups($filter,
831
+            [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
832
+        if (is_array($groups)) {
833
+            $fetcher = function ($dn, &$seen) {
834
+                if (is_array($dn) && isset($dn['dn'][0])) {
835
+                    $dn = $dn['dn'][0];
836
+                }
837
+                return $this->getGroupsByMember($dn, $seen);
838
+            };
839
+
840
+            if (empty($dn)) {
841
+                $dn = "";
842
+            }
843
+
844
+            $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
845
+        }
846
+        $visibleGroups = $this->filterValidGroups($allGroups);
847
+        return array_intersect_key($allGroups, $visibleGroups);
848
+    }
849
+
850
+    /**
851
+     * get a list of all users in a group
852
+     *
853
+     * @param string $gid
854
+     * @param string $search
855
+     * @param int $limit
856
+     * @param int $offset
857
+     * @return array with user ids
858
+     * @throws Exception
859
+     * @throws ServerNotAvailableException
860
+     */
861
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
862
+        if (!$this->enabled) {
863
+            return [];
864
+        }
865
+        if (!$this->groupExists($gid)) {
866
+            return [];
867
+        }
868
+        $search = $this->access->escapeFilterPart($search, true);
869
+        $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
870
+        // check for cache of the exact query
871
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
872
+        if (!is_null($groupUsers)) {
873
+            return $groupUsers;
874
+        }
875
+
876
+        if ($limit === -1) {
877
+            $limit = null;
878
+        }
879
+        // check for cache of the query without limit and offset
880
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
881
+        if (!is_null($groupUsers)) {
882
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
883
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
884
+            return $groupUsers;
885
+        }
886
+
887
+        $groupDN = $this->access->groupname2dn($gid);
888
+        if (!$groupDN) {
889
+            // group couldn't be found, return empty resultset
890
+            $this->access->connection->writeToCache($cacheKey, []);
891
+            return [];
892
+        }
893
+
894
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
895
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
896
+        $members = $this->_groupMembers($groupDN);
897
+        if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
898
+            //in case users could not be retrieved, return empty result set
899
+            $this->access->connection->writeToCache($cacheKey, []);
900
+            return [];
901
+        }
902
+
903
+        $groupUsers = [];
904
+        $attrs = $this->access->userManager->getAttributes(true);
905
+        foreach ($members as $member) {
906
+            switch ($this->ldapGroupMemberAssocAttr) {
907
+                case 'zimbramailforwardingaddress':
908
+                    //we get email addresses and need to convert them to uids
909
+                    $parts = explode('@', $member);
910
+                    $member = $parts[0];
911
+                    //no break needed because we just needed to remove the email part and now we have uids
912
+                case 'memberuid':
913
+                    //we got uids, need to get their DNs to 'translate' them to user names
914
+                    $filter = $this->access->combineFilterWithAnd([
915
+                        str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
916
+                        $this->access->combineFilterWithAnd([
917
+                            $this->access->getFilterPartForUserSearch($search),
918
+                            $this->access->connection->ldapUserFilter
919
+                        ])
920
+                    ]);
921
+                    $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
922
+                    if (empty($ldap_users)) {
923
+                        break;
924
+                    }
925
+                    $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
926
+                    break;
927
+                default:
928
+                    //we got DNs, check if we need to filter by search or we can give back all of them
929
+                    $uid = $this->access->dn2username($member);
930
+                    if (!$uid) {
931
+                        break;
932
+                    }
933
+
934
+                    $cacheKey = 'userExistsOnLDAP' . $uid;
935
+                    $userExists = $this->access->connection->getFromCache($cacheKey);
936
+                    if ($userExists === false) {
937
+                        break;
938
+                    }
939
+                    if ($userExists === null || $search !== '') {
940
+                        if (!$this->access->readAttribute($member,
941
+                            $this->access->connection->ldapUserDisplayName,
942
+                            $this->access->combineFilterWithAnd([
943
+                                $this->access->getFilterPartForUserSearch($search),
944
+                                $this->access->connection->ldapUserFilter
945
+                            ]))) {
946
+                            if ($search === '') {
947
+                                $this->access->connection->writeToCache($cacheKey, false);
948
+                            }
949
+                            break;
950
+                        }
951
+                        $this->access->connection->writeToCache($cacheKey, true);
952
+                    }
953
+                    $groupUsers[] = $uid;
954
+                    break;
955
+            }
956
+        }
957
+
958
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
959
+        natsort($groupUsers);
960
+        $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
961
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
962
+
963
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
964
+
965
+        return $groupUsers;
966
+    }
967
+
968
+    /**
969
+     * returns the number of users in a group, who match the search term
970
+     *
971
+     * @param string $gid the internal group name
972
+     * @param string $search optional, a search string
973
+     * @return int|bool
974
+     * @throws Exception
975
+     * @throws ServerNotAvailableException
976
+     */
977
+    public function countUsersInGroup($gid, $search = '') {
978
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
979
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
980
+        }
981
+
982
+        $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
983
+        if (!$this->enabled || !$this->groupExists($gid)) {
984
+            return false;
985
+        }
986
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
987
+        if (!is_null($groupUsers)) {
988
+            return $groupUsers;
989
+        }
990
+
991
+        $groupDN = $this->access->groupname2dn($gid);
992
+        if (!$groupDN) {
993
+            // group couldn't be found, return empty result set
994
+            $this->access->connection->writeToCache($cacheKey, false);
995
+            return false;
996
+        }
997
+
998
+        $members = $this->_groupMembers($groupDN);
999
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
1000
+        if (!$members && $primaryUserCount === 0) {
1001
+            //in case users could not be retrieved, return empty result set
1002
+            $this->access->connection->writeToCache($cacheKey, false);
1003
+            return false;
1004
+        }
1005
+
1006
+        if ($search === '') {
1007
+            $groupUsers = count($members) + $primaryUserCount;
1008
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
1009
+            return $groupUsers;
1010
+        }
1011
+        $search = $this->access->escapeFilterPart($search, true);
1012
+        $isMemberUid =
1013
+            ($this->ldapGroupMemberAssocAttr === 'memberuid' ||
1014
+                $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
1015
+
1016
+        //we need to apply the search filter
1017
+        //alternatives that need to be checked:
1018
+        //a) get all users by search filter and array_intersect them
1019
+        //b) a, but only when less than 1k 10k ?k users like it is
1020
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
1021
+        //   and let it count.
1022
+        //For now this is not important, because the only use of this method
1023
+        //does not supply a search string
1024
+        $groupUsers = [];
1025
+        foreach ($members as $member) {
1026
+            if ($isMemberUid) {
1027
+                if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
1028
+                    //we get email addresses and need to convert them to uids
1029
+                    $parts = explode('@', $member);
1030
+                    $member = $parts[0];
1031
+                }
1032
+                //we got uids, need to get their DNs to 'translate' them to user names
1033
+                $filter = $this->access->combineFilterWithAnd([
1034
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
1035
+                    $this->access->getFilterPartForUserSearch($search)
1036
+                ]);
1037
+                $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
1038
+                if (count($ldap_users) < 1) {
1039
+                    continue;
1040
+                }
1041
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
1042
+            } else {
1043
+                //we need to apply the search filter now
1044
+                if (!$this->access->readAttribute($member,
1045
+                    $this->access->connection->ldapUserDisplayName,
1046
+                    $this->access->getFilterPartForUserSearch($search))) {
1047
+                    continue;
1048
+                }
1049
+                // dn2username will also check if the users belong to the allowed base
1050
+                if ($ncGroupId = $this->access->dn2username($member)) {
1051
+                    $groupUsers[] = $ncGroupId;
1052
+                }
1053
+            }
1054
+        }
1055
+
1056
+        //and get users that have the group as primary
1057
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1058
+
1059
+        return count($groupUsers) + $primaryUsers;
1060
+    }
1061
+
1062
+    /**
1063
+     * get a list of all groups using a paged search
1064
+     *
1065
+     * @param string $search
1066
+     * @param int $limit
1067
+     * @param int $offset
1068
+     * @return array with group names
1069
+     *
1070
+     * Returns a list with all groups
1071
+     * Uses a paged search if available to override a
1072
+     * server side search limit.
1073
+     * (active directory has a limit of 1000 by default)
1074
+     * @throws Exception
1075
+     */
1076
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1077
+        if (!$this->enabled) {
1078
+            return [];
1079
+        }
1080
+        $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1081
+
1082
+        //Check cache before driving unnecessary searches
1083
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
1084
+        if (!is_null($ldap_groups)) {
1085
+            return $ldap_groups;
1086
+        }
1087
+
1088
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
1089
+        // error. With a limit of 0, we get 0 results. So we pass null.
1090
+        if ($limit <= 0) {
1091
+            $limit = null;
1092
+        }
1093
+        $filter = $this->access->combineFilterWithAnd([
1094
+            $this->access->connection->ldapGroupFilter,
1095
+            $this->access->getFilterPartForGroupSearch($search)
1096
+        ]);
1097
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
1098
+            [$this->access->connection->ldapGroupDisplayName, 'dn'],
1099
+            $limit,
1100
+            $offset);
1101
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1102
+
1103
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
1104
+        return $ldap_groups;
1105
+    }
1106
+
1107
+    /**
1108
+     * check if a group exists
1109
+     *
1110
+     * @param string $gid
1111
+     * @return bool
1112
+     * @throws ServerNotAvailableException
1113
+     */
1114
+    public function groupExists($gid) {
1115
+        $groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1116
+        if (!is_null($groupExists)) {
1117
+            return (bool)$groupExists;
1118
+        }
1119
+
1120
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1121
+        //only, requires more checking.
1122
+        $dn = $this->access->groupname2dn($gid);
1123
+        if (!$dn) {
1124
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1125
+            return false;
1126
+        }
1127
+
1128
+        if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1129
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1130
+            return false;
1131
+        }
1132
+
1133
+        //if group really still exists, we will be able to read its objectClass
1134
+        if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1135
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1136
+            return false;
1137
+        }
1138
+
1139
+        $this->access->connection->writeToCache('groupExists' . $gid, true);
1140
+        return true;
1141
+    }
1142
+
1143
+    /**
1144
+     * @throws ServerNotAvailableException
1145
+     * @throws Exception
1146
+     */
1147
+    protected function filterValidGroups(array $listOfGroups): array {
1148
+        $validGroupDNs = [];
1149
+        foreach ($listOfGroups as $key => $item) {
1150
+            $dn = is_string($item) ? $item : $item['dn'][0];
1151
+            $gid = $this->access->dn2groupname($dn);
1152
+            if (!$gid) {
1153
+                continue;
1154
+            }
1155
+            if ($this->groupExists($gid)) {
1156
+                $validGroupDNs[$key] = $item;
1157
+            }
1158
+        }
1159
+        return $validGroupDNs;
1160
+    }
1161
+
1162
+    /**
1163
+     * Check if backend implements actions
1164
+     *
1165
+     * @param int $actions bitwise-or'ed actions
1166
+     * @return boolean
1167
+     *
1168
+     * Returns the supported actions as int to be
1169
+     * compared with GroupInterface::CREATE_GROUP etc.
1170
+     */
1171
+    public function implementsActions($actions) {
1172
+        return (bool)((GroupInterface::COUNT_USERS |
1173
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1174
+    }
1175
+
1176
+    /**
1177
+     * Return access for LDAP interaction.
1178
+     *
1179
+     * @return Access instance of Access for LDAP interaction
1180
+     */
1181
+    public function getLDAPAccess($gid) {
1182
+        return $this->access;
1183
+    }
1184
+
1185
+    /**
1186
+     * create a group
1187
+     *
1188
+     * @param string $gid
1189
+     * @return bool
1190
+     * @throws Exception
1191
+     * @throws ServerNotAvailableException
1192
+     */
1193
+    public function createGroup($gid) {
1194
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1195
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1196
+                //updates group mapping
1197
+                $uuid = $this->access->getUUID($dn, false);
1198
+                if (is_string($uuid)) {
1199
+                    $this->access->mapAndAnnounceIfApplicable(
1200
+                        $this->access->getGroupMapper(),
1201
+                        $dn,
1202
+                        $gid,
1203
+                        $uuid,
1204
+                        false
1205
+                    );
1206
+                    $this->access->cacheGroupExists($gid);
1207
+                }
1208
+            }
1209
+            return $dn != null;
1210
+        }
1211
+        throw new Exception('Could not create group in LDAP backend.');
1212
+    }
1213
+
1214
+    /**
1215
+     * delete a group
1216
+     *
1217
+     * @param string $gid gid of the group to delete
1218
+     * @return bool
1219
+     * @throws Exception
1220
+     */
1221
+    public function deleteGroup($gid) {
1222
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1223
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1224
+                #delete group in nextcloud internal db
1225
+                $this->access->getGroupMapper()->unmap($gid);
1226
+                $this->access->connection->writeToCache("groupExists" . $gid, false);
1227
+            }
1228
+            return $ret;
1229
+        }
1230
+        throw new Exception('Could not delete group in LDAP backend.');
1231
+    }
1232
+
1233
+    /**
1234
+     * Add a user to a group
1235
+     *
1236
+     * @param string $uid Name of the user to add to group
1237
+     * @param string $gid Name of the group in which add the user
1238
+     * @return bool
1239
+     * @throws Exception
1240
+     */
1241
+    public function addToGroup($uid, $gid) {
1242
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1243
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1244
+                $this->access->connection->clearCache();
1245
+                unset($this->cachedGroupMembers[$gid]);
1246
+            }
1247
+            return $ret;
1248
+        }
1249
+        throw new Exception('Could not add user to group in LDAP backend.');
1250
+    }
1251
+
1252
+    /**
1253
+     * Removes a user from a group
1254
+     *
1255
+     * @param string $uid Name of the user to remove from group
1256
+     * @param string $gid Name of the group from which remove the user
1257
+     * @return bool
1258
+     * @throws Exception
1259
+     */
1260
+    public function removeFromGroup($uid, $gid) {
1261
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1262
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1263
+                $this->access->connection->clearCache();
1264
+                unset($this->cachedGroupMembers[$gid]);
1265
+            }
1266
+            return $ret;
1267
+        }
1268
+        throw new Exception('Could not remove user from group in LDAP backend.');
1269
+    }
1270
+
1271
+    /**
1272
+     * Gets group details
1273
+     *
1274
+     * @param string $gid Name of the group
1275
+     * @return array|false
1276
+     * @throws Exception
1277
+     */
1278
+    public function getGroupDetails($gid) {
1279
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1280
+            return $this->groupPluginManager->getGroupDetails($gid);
1281
+        }
1282
+        throw new Exception('Could not get group details in LDAP backend.');
1283
+    }
1284
+
1285
+    /**
1286
+     * Return LDAP connection resource from a cloned connection.
1287
+     * The cloned connection needs to be closed manually.
1288
+     * of the current access.
1289
+     *
1290
+     * @param string $gid
1291
+     * @return resource of the LDAP connection
1292
+     * @throws ServerNotAvailableException
1293
+     */
1294
+    public function getNewLDAPConnection($gid) {
1295
+        $connection = clone $this->access->getConnection();
1296
+        return $connection->getConnectionResource();
1297
+    }
1298
+
1299
+    /**
1300
+     * @throws ServerNotAvailableException
1301
+     */
1302
+    public function getDisplayName(string $gid): string {
1303
+        if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1304
+            return $this->groupPluginManager->getDisplayName($gid);
1305
+        }
1306
+
1307
+        $cacheKey = 'group_getDisplayName' . $gid;
1308
+        if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1309
+            return $displayName;
1310
+        }
1311
+
1312
+        $displayName = $this->access->readAttribute(
1313
+            $this->access->groupname2dn($gid),
1314
+            $this->access->connection->ldapGroupDisplayName);
1315
+
1316
+        if ($displayName && (count($displayName) > 0)) {
1317
+            $displayName = $displayName[0];
1318
+            $this->access->connection->writeToCache($cacheKey, $displayName);
1319
+            return $displayName;
1320
+        }
1321
+
1322
+        return '';
1323
+    }
1324 1324
 }
Please login to merge, or discard this patch.
Spacing   +38 added lines, -38 removed lines patch added patch discarded remove patch
@@ -100,10 +100,10 @@  discard block
 block discarded – undo
100 100
 		if (!$this->enabled) {
101 101
 			return false;
102 102
 		}
103
-		$cacheKey = 'inGroup' . $uid . ':' . $gid;
103
+		$cacheKey = 'inGroup'.$uid.':'.$gid;
104 104
 		$inGroup = $this->access->connection->getFromCache($cacheKey);
105 105
 		if (!is_null($inGroup)) {
106
-			return (bool)$inGroup;
106
+			return (bool) $inGroup;
107 107
 		}
108 108
 
109 109
 		$userDN = $this->access->username2dn($uid);
@@ -112,7 +112,7 @@  discard block
 block discarded – undo
112 112
 			return in_array($userDN, $this->cachedGroupMembers[$gid]);
113 113
 		}
114 114
 
115
-		$cacheKeyMembers = 'inGroup-members:' . $gid;
115
+		$cacheKeyMembers = 'inGroup-members:'.$gid;
116 116
 		$members = $this->access->connection->getFromCache($cacheKeyMembers);
117 117
 		if (!is_null($members)) {
118 118
 			$this->cachedGroupMembers[$gid] = $members;
@@ -240,7 +240,7 @@  discard block
 block discarded – undo
240 240
 			return [];
241 241
 		}
242 242
 		// used extensively in cron job, caching makes sense for nested groups
243
-		$cacheKey = '_groupMembers' . $dnGroup;
243
+		$cacheKey = '_groupMembers'.$dnGroup;
244 244
 		$groupMembers = $this->access->connection->getFromCache($cacheKey);
245 245
 		if ($groupMembers !== null) {
246 246
 			return $groupMembers;
@@ -255,14 +255,14 @@  discard block
 block discarded – undo
255 255
 			// compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
256 256
 			$filter = $this->access->combineFilterWithAnd([
257 257
 				$this->access->connection->ldapUserFilter,
258
-				$this->access->connection->ldapUserDisplayName . '=*',
259
-				'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
258
+				$this->access->connection->ldapUserDisplayName.'=*',
259
+				'memberof:1.2.840.113556.1.4.1941:='.$dnGroup
260 260
 			]);
261 261
 			$memberRecords = $this->access->fetchListOfUsers(
262 262
 				$filter,
263 263
 				$this->access->userManager->getAttributes(true)
264 264
 			);
265
-			$result = array_reduce($memberRecords, function ($carry, $record) {
265
+			$result = array_reduce($memberRecords, function($carry, $record) {
266 266
 				$carry[] = $record['dn'][0];
267 267
 				return $carry;
268 268
 			}, []);
@@ -279,7 +279,7 @@  discard block
 block discarded – undo
279 279
 		$seen[$dnGroup] = 1;
280 280
 		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
281 281
 		if (is_array($members)) {
282
-			$fetcher = function ($memberDN, &$seen) {
282
+			$fetcher = function($memberDN, &$seen) {
283 283
 				return $this->_groupMembers($memberDN, $seen);
284 284
 			};
285 285
 			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
@@ -307,7 +307,7 @@  discard block
 block discarded – undo
307 307
 			return [];
308 308
 		}
309 309
 
310
-		$fetcher = function ($groupDN) {
310
+		$fetcher = function($groupDN) {
311 311
 			if (isset($this->cachedNestedGroups[$groupDN])) {
312 312
 				$nestedGroups = $this->cachedNestedGroups[$groupDN];
313 313
 			} else {
@@ -325,7 +325,7 @@  discard block
 block discarded – undo
325 325
 	}
326 326
 
327 327
 	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
328
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
328
+		$nesting = (int) $this->access->connection->ldapNestedGroups;
329 329
 		// depending on the input, we either have a list of DNs or a list of LDAP records
330 330
 		// also, the output expects either DNs or records. Testing the first element should suffice.
331 331
 		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
@@ -333,7 +333,7 @@  discard block
 block discarded – undo
333 333
 		if ($nesting !== 1) {
334 334
 			if ($recordMode) {
335 335
 				// the keys are numeric, but should hold the DN
336
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
336
+				return array_reduce($list, function($transformed, $record) use ($dn) {
337 337
 					if ($record['dn'][0] != $dn) {
338 338
 						$transformed[$record['dn'][0]] = $record;
339 339
 					}
@@ -366,7 +366,7 @@  discard block
 block discarded – undo
366 366
 	 * @throws ServerNotAvailableException
367 367
 	 */
368 368
 	public function gidNumber2Name(string $gid, string $dn) {
369
-		$cacheKey = 'gidNumberToName' . $gid;
369
+		$cacheKey = 'gidNumberToName'.$gid;
370 370
 		$groupName = $this->access->connection->getFromCache($cacheKey);
371 371
 		if (!is_null($groupName) && isset($groupName)) {
372 372
 			return $groupName;
@@ -376,7 +376,7 @@  discard block
 block discarded – undo
376 376
 		$filter = $this->access->combineFilterWithAnd([
377 377
 			$this->access->connection->ldapGroupFilter,
378 378
 			'objectClass=posixGroup',
379
-			$this->access->connection->ldapGidNumber . '=' . $gid
379
+			$this->access->connection->ldapGidNumber.'='.$gid
380 380
 		]);
381 381
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
382 382
 	}
@@ -456,7 +456,7 @@  discard block
 block discarded – undo
456 456
 		if ($search !== '') {
457 457
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
458 458
 		}
459
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
459
+		$filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID;
460 460
 
461 461
 		return $this->access->combineFilterWithAnd($filterParts);
462 462
 	}
@@ -526,7 +526,7 @@  discard block
 block discarded – undo
526 526
 		//we need to get the DN from LDAP
527 527
 		$filter = $this->access->combineFilterWithAnd([
528 528
 			$this->access->connection->ldapGroupFilter,
529
-			'objectsid=' . $domainObjectSid . '-' . $gid
529
+			'objectsid='.$domainObjectSid.'-'.$gid
530 530
 		]);
531 531
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
532 532
 	}
@@ -583,7 +583,7 @@  discard block
 block discarded – undo
583 583
 		if ($search !== '') {
584 584
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
585 585
 		}
586
-		$filterParts[] = 'primaryGroupID=' . $groupID;
586
+		$filterParts[] = 'primaryGroupID='.$groupID;
587 587
 
588 588
 		return $this->access->combineFilterWithAnd($filterParts);
589 589
 	}
@@ -625,7 +625,7 @@  discard block
 block discarded – undo
625 625
 		try {
626 626
 			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
627 627
 			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
628
-			return (int)$users;
628
+			return (int) $users;
629 629
 		} catch (ServerNotAvailableException $e) {
630 630
 			throw $e;
631 631
 		} catch (Exception $e) {
@@ -664,7 +664,7 @@  discard block
 block discarded – undo
664 664
 		if (!$this->enabled) {
665 665
 			return [];
666 666
 		}
667
-		$cacheKey = 'getUserGroups' . $uid;
667
+		$cacheKey = 'getUserGroups'.$uid;
668 668
 		$userGroups = $this->access->connection->getFromCache($cacheKey);
669 669
 		if (!is_null($userGroups)) {
670 670
 			return $userGroups;
@@ -722,8 +722,8 @@  discard block
 block discarded – undo
722 722
 		// if possible, read out membership via memberOf. It's far faster than
723 723
 		// performing a search, which still is a fallback later.
724 724
 		// memberof doesn't support memberuid, so skip it here.
725
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
726
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
725
+		if ((int) $this->access->connection->hasMemberOfFilterSupport === 1
726
+			&& (int) $this->access->connection->useMemberOfToDetectMembership === 1
727 727
 			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
728 728
 			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
729 729
 			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
@@ -815,14 +815,14 @@  discard block
 block discarded – undo
815 815
 		}
816 816
 		$allGroups = [];
817 817
 		$seen[$dn] = true;
818
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
818
+		$filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
819 819
 
820 820
 		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
821 821
 			//in this case the member entries are email addresses
822 822
 			$filter .= '@*';
823 823
 		}
824 824
 
825
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
825
+		$nesting = (int) $this->access->connection->ldapNestedGroups;
826 826
 		if ($nesting === 0) {
827 827
 			$filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
828 828
 		}
@@ -830,7 +830,7 @@  discard block
 block discarded – undo
830 830
 		$groups = $this->access->fetchListOfGroups($filter,
831 831
 			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
832 832
 		if (is_array($groups)) {
833
-			$fetcher = function ($dn, &$seen) {
833
+			$fetcher = function($dn, &$seen) {
834 834
 				if (is_array($dn) && isset($dn['dn'][0])) {
835 835
 					$dn = $dn['dn'][0];
836 836
 				}
@@ -866,7 +866,7 @@  discard block
 block discarded – undo
866 866
 			return [];
867 867
 		}
868 868
 		$search = $this->access->escapeFilterPart($search, true);
869
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
869
+		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
870 870
 		// check for cache of the exact query
871 871
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
872 872
 		if (!is_null($groupUsers)) {
@@ -877,7 +877,7 @@  discard block
 block discarded – undo
877 877
 			$limit = null;
878 878
 		}
879 879
 		// check for cache of the query without limit and offset
880
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
880
+		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
881 881
 		if (!is_null($groupUsers)) {
882 882
 			$groupUsers = array_slice($groupUsers, $offset, $limit);
883 883
 			$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -931,7 +931,7 @@  discard block
 block discarded – undo
931 931
 						break;
932 932
 					}
933 933
 
934
-					$cacheKey = 'userExistsOnLDAP' . $uid;
934
+					$cacheKey = 'userExistsOnLDAP'.$uid;
935 935
 					$userExists = $this->access->connection->getFromCache($cacheKey);
936 936
 					if ($userExists === false) {
937 937
 						break;
@@ -957,7 +957,7 @@  discard block
 block discarded – undo
957 957
 
958 958
 		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
959 959
 		natsort($groupUsers);
960
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
960
+		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
961 961
 		$groupUsers = array_slice($groupUsers, $offset, $limit);
962 962
 
963 963
 		$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -979,7 +979,7 @@  discard block
 block discarded – undo
979 979
 			return $this->groupPluginManager->countUsersInGroup($gid, $search);
980 980
 		}
981 981
 
982
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
982
+		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
983 983
 		if (!$this->enabled || !$this->groupExists($gid)) {
984 984
 			return false;
985 985
 		}
@@ -1077,7 +1077,7 @@  discard block
 block discarded – undo
1077 1077
 		if (!$this->enabled) {
1078 1078
 			return [];
1079 1079
 		}
1080
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1080
+		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
1081 1081
 
1082 1082
 		//Check cache before driving unnecessary searches
1083 1083
 		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
@@ -1112,31 +1112,31 @@  discard block
 block discarded – undo
1112 1112
 	 * @throws ServerNotAvailableException
1113 1113
 	 */
1114 1114
 	public function groupExists($gid) {
1115
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1115
+		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1116 1116
 		if (!is_null($groupExists)) {
1117
-			return (bool)$groupExists;
1117
+			return (bool) $groupExists;
1118 1118
 		}
1119 1119
 
1120 1120
 		//getting dn, if false the group does not exist. If dn, it may be mapped
1121 1121
 		//only, requires more checking.
1122 1122
 		$dn = $this->access->groupname2dn($gid);
1123 1123
 		if (!$dn) {
1124
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1124
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1125 1125
 			return false;
1126 1126
 		}
1127 1127
 
1128 1128
 		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1129
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1129
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1130 1130
 			return false;
1131 1131
 		}
1132 1132
 
1133 1133
 		//if group really still exists, we will be able to read its objectClass
1134 1134
 		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1135
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1135
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1136 1136
 			return false;
1137 1137
 		}
1138 1138
 
1139
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1139
+		$this->access->connection->writeToCache('groupExists'.$gid, true);
1140 1140
 		return true;
1141 1141
 	}
1142 1142
 
@@ -1169,7 +1169,7 @@  discard block
 block discarded – undo
1169 1169
 	 * compared with GroupInterface::CREATE_GROUP etc.
1170 1170
 	 */
1171 1171
 	public function implementsActions($actions) {
1172
-		return (bool)((GroupInterface::COUNT_USERS |
1172
+		return (bool) ((GroupInterface::COUNT_USERS |
1173 1173
 				$this->groupPluginManager->getImplementedActions()) & $actions);
1174 1174
 	}
1175 1175
 
@@ -1223,7 +1223,7 @@  discard block
 block discarded – undo
1223 1223
 			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1224 1224
 				#delete group in nextcloud internal db
1225 1225
 				$this->access->getGroupMapper()->unmap($gid);
1226
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1226
+				$this->access->connection->writeToCache("groupExists".$gid, false);
1227 1227
 			}
1228 1228
 			return $ret;
1229 1229
 		}
@@ -1304,7 +1304,7 @@  discard block
 block discarded – undo
1304 1304
 			return $this->groupPluginManager->getDisplayName($gid);
1305 1305
 		}
1306 1306
 
1307
-		$cacheKey = 'group_getDisplayName' . $gid;
1307
+		$cacheKey = 'group_getDisplayName'.$gid;
1308 1308
 		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1309 1309
 			return $displayName;
1310 1310
 		}
Please login to merge, or discard this patch.