Passed
Push — master ( 01f082...9cd39b )
by Blizzz
19:42 queued 06:16
created
apps/user_ldap/lib/Group_LDAP.php 2 patches
Indentation   +1292 added lines, -1292 removed lines patch added patch discarded remove patch
@@ -53,1296 +53,1296 @@
 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
-
140
-		//extra work if we don't get back user DNs
141
-		switch ($this->ldapGroupMemberAssocAttr) {
142
-			case 'memberuid':
143
-			case 'zimbramailforwardingaddress':
144
-				$requestAttributes = $this->access->userManager->getAttributes(true);
145
-				$users = [];
146
-				$filterParts = [];
147
-				$bytes = 0;
148
-				foreach ($members as $mid) {
149
-					if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
150
-						$parts = explode('@', $mid); //making sure we get only the uid
151
-						$mid = $parts[0];
152
-					}
153
-					$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
154
-					$filterParts[] = $filter;
155
-					$bytes += strlen($filter);
156
-					if ($bytes >= 9000000) {
157
-						// AD has a default input buffer of 10 MB, we do not want
158
-						// to take even the chance to exceed it
159
-						// so we fetch results with the filterParts we collected so far
160
-						$filter = $this->access->combineFilterWithOr($filterParts);
161
-						$search = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
162
-						$bytes = 0;
163
-						$filterParts = [];
164
-						$users = array_merge($users, $search);
165
-					}
166
-				}
167
-
168
-				if (count($filterParts) > 0) {
169
-					// if there are filterParts left we need to add their result
170
-					$filter = $this->access->combineFilterWithOr($filterParts);
171
-					$search = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
172
-					$users = array_merge($users, $search);
173
-				}
174
-
175
-				// now we cleanup the users array to get only dns
176
-				$dns = [];
177
-				foreach ($users as $record) {
178
-					$dns[$record['dn'][0]] = 1;
179
-				}
180
-				$members = array_keys($dns);
181
-
182
-				break;
183
-		}
184
-
185
-		if (count($members) === 0) {
186
-			$this->access->connection->writeToCache($cacheKey, false);
187
-			return false;
188
-		}
189
-
190
-		$isInGroup = in_array($userDN, $members);
191
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
192
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
193
-		$this->cachedGroupMembers[$gid] = $members;
194
-
195
-		return $isInGroup;
196
-	}
197
-
198
-	/**
199
-	 * For a group that has user membership defined by an LDAP search url
200
-	 * attribute returns the users that match the search url otherwise returns
201
-	 * an empty array.
202
-	 *
203
-	 * @throws ServerNotAvailableException
204
-	 */
205
-	public function getDynamicGroupMembers(string $dnGroup): array {
206
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
207
-
208
-		if (empty($dynamicGroupMemberURL)) {
209
-			return [];
210
-		}
211
-
212
-		$dynamicMembers = [];
213
-		$memberURLs = $this->access->readAttribute(
214
-			$dnGroup,
215
-			$dynamicGroupMemberURL,
216
-			$this->access->connection->ldapGroupFilter
217
-		);
218
-		if ($memberURLs !== false) {
219
-			// this group has the 'memberURL' attribute so this is a dynamic group
220
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
221
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
222
-			$pos = strpos($memberURLs[0], '(');
223
-			if ($pos !== false) {
224
-				$memberUrlFilter = substr($memberURLs[0], $pos);
225
-				$foundMembers = $this->access->searchUsers($memberUrlFilter, ['dn']);
226
-				$dynamicMembers = [];
227
-				foreach ($foundMembers as $value) {
228
-					$dynamicMembers[$value['dn'][0]] = 1;
229
-				}
230
-			} else {
231
-				$this->logger->debug('No search filter found on member url of group {dn}',
232
-					[
233
-						'app' => 'user_ldap',
234
-						'dn' => $dnGroup,
235
-					]
236
-				);
237
-			}
238
-		}
239
-		return $dynamicMembers;
240
-	}
241
-
242
-	/**
243
-	 * @throws ServerNotAvailableException
244
-	 */
245
-	private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
246
-		if ($seen === null) {
247
-			$seen = [];
248
-			// the root entry has to be marked as processed to avoind infinit loops,
249
-			// but not included in the results laters on
250
-			$excludeFromResult = $dnGroup;
251
-		}
252
-		$allMembers = [];
253
-		if (array_key_exists($dnGroup, $seen)) {
254
-			return [];
255
-		}
256
-		// used extensively in cron job, caching makes sense for nested groups
257
-		$cacheKey = '_groupMembers' . $dnGroup;
258
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
259
-		if ($groupMembers !== null) {
260
-			return $groupMembers;
261
-		}
262
-
263
-		if ($this->access->connection->ldapNestedGroups
264
-			&& $this->access->connection->useMemberOfToDetectMembership
265
-			&& $this->access->connection->hasMemberOfFilterSupport
266
-			&& $this->access->connection->ldapMatchingRuleInChainState !== Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE
267
-		) {
268
-			$attemptedLdapMatchingRuleInChain = true;
269
-			// compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
270
-			$filter = $this->access->combineFilterWithAnd([
271
-				$this->access->connection->ldapUserFilter,
272
-				$this->access->connection->ldapUserDisplayName . '=*',
273
-				'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
274
-			]);
275
-			$memberRecords = $this->access->fetchListOfUsers(
276
-				$filter,
277
-				$this->access->userManager->getAttributes(true)
278
-			);
279
-			$result = array_reduce($memberRecords, function ($carry, $record) {
280
-				$carry[] = $record['dn'][0];
281
-				return $carry;
282
-			}, []);
283
-			if ($this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_AVAILABLE) {
284
-				return $result;
285
-			} elseif (!empty($memberRecords)) {
286
-				$this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_AVAILABLE;
287
-				$this->access->connection->saveConfiguration();
288
-				return $result;
289
-			}
290
-			// when feature availability is unknown, and the result is empty, continue and test with original approach
291
-		}
292
-
293
-		$seen[$dnGroup] = 1;
294
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
295
-		if (is_array($members)) {
296
-			$fetcher = function ($memberDN) use (&$seen) {
297
-				return $this->_groupMembers($memberDN, $seen);
298
-			};
299
-			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members, $seen);
300
-		}
301
-
302
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
303
-		if (isset($excludeFromResult)) {
304
-			$index = array_search($excludeFromResult, $allMembers, true);
305
-			if ($index !== false) {
306
-				unset($allMembers[$index]);
307
-			}
308
-		}
309
-
310
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
311
-		if (isset($attemptedLdapMatchingRuleInChain)
312
-			&& $this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_UNKNOWN
313
-			&& !empty($allMembers)
314
-		) {
315
-			$this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE;
316
-			$this->access->connection->saveConfiguration();
317
-		}
318
-		return $allMembers;
319
-	}
320
-
321
-	/**
322
-	 * @throws ServerNotAvailableException
323
-	 */
324
-	private function _getGroupDNsFromMemberOf(string $dn): array {
325
-		$groups = $this->access->readAttribute($dn, 'memberOf');
326
-		if (!is_array($groups)) {
327
-			return [];
328
-		}
329
-
330
-		$fetcher = function ($groupDN) {
331
-			if (isset($this->cachedNestedGroups[$groupDN])) {
332
-				$nestedGroups = $this->cachedNestedGroups[$groupDN];
333
-			} else {
334
-				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
335
-				if (!is_array($nestedGroups)) {
336
-					$nestedGroups = [];
337
-				}
338
-				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
339
-			}
340
-			return $nestedGroups;
341
-		};
342
-
343
-		$groups = $this->walkNestedGroups($dn, $fetcher, $groups);
344
-		return $this->filterValidGroups($groups);
345
-	}
346
-
347
-	private function walkNestedGroups(string $dn, Closure $fetcher, array $list, array &$seen = []): array {
348
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
349
-		// depending on the input, we either have a list of DNs or a list of LDAP records
350
-		// also, the output expects either DNs or records. Testing the first element should suffice.
351
-		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
352
-
353
-		if ($nesting !== 1) {
354
-			if ($recordMode) {
355
-				// the keys are numeric, but should hold the DN
356
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
357
-					if ($record['dn'][0] != $dn) {
358
-						$transformed[$record['dn'][0]] = $record;
359
-					}
360
-					return $transformed;
361
-				}, []);
362
-			}
363
-			return $list;
364
-		}
365
-
366
-		while ($record = array_shift($list)) {
367
-			$recordDN = $record['dn'][0] ?? $record;
368
-			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
369
-				// Prevent loops
370
-				continue;
371
-			}
372
-			$fetched = $fetcher($record);
373
-			$list = array_merge($list, $fetched);
374
-			if (!isset($seen[$recordDN]) || is_bool($seen[$recordDN]) && is_array($record)) {
375
-				$seen[$recordDN] = $record;
376
-			}
377
-		}
378
-
379
-		// on record mode, filter out intermediate state
380
-		return $recordMode ? array_filter($seen, 'is_array') : array_keys($seen);
381
-	}
382
-
383
-	/**
384
-	 * translates a gidNumber into an ownCloud internal name
385
-	 *
386
-	 * @return string|bool
387
-	 * @throws Exception
388
-	 * @throws ServerNotAvailableException
389
-	 */
390
-	public function gidNumber2Name(string $gid, string $dn) {
391
-		$cacheKey = 'gidNumberToName' . $gid;
392
-		$groupName = $this->access->connection->getFromCache($cacheKey);
393
-		if (!is_null($groupName) && isset($groupName)) {
394
-			return $groupName;
395
-		}
396
-
397
-		//we need to get the DN from LDAP
398
-		$filter = $this->access->combineFilterWithAnd([
399
-			$this->access->connection->ldapGroupFilter,
400
-			'objectClass=posixGroup',
401
-			$this->access->connection->ldapGidNumber . '=' . $gid
402
-		]);
403
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
404
-	}
405
-
406
-	/**
407
-	 * @throws ServerNotAvailableException
408
-	 * @throws Exception
409
-	 */
410
-	private function getNameOfGroup(string $filter, string $cacheKey) {
411
-		$result = $this->access->searchGroups($filter, ['dn'], 1);
412
-		if (empty($result)) {
413
-			return null;
414
-		}
415
-		$dn = $result[0]['dn'][0];
416
-
417
-		//and now the group name
418
-		//NOTE once we have separate Nextcloud group IDs and group names we can
419
-		//directly read the display name attribute instead of the DN
420
-		$name = $this->access->dn2groupname($dn);
421
-
422
-		$this->access->connection->writeToCache($cacheKey, $name);
423
-
424
-		return $name;
425
-	}
426
-
427
-	/**
428
-	 * returns the entry's gidNumber
429
-	 *
430
-	 * @return string|bool
431
-	 * @throws ServerNotAvailableException
432
-	 */
433
-	private function getEntryGidNumber(string $dn, string $attribute) {
434
-		$value = $this->access->readAttribute($dn, $attribute);
435
-		if (is_array($value) && !empty($value)) {
436
-			return $value[0];
437
-		}
438
-		return false;
439
-	}
440
-
441
-	/**
442
-	 * @return string|bool
443
-	 * @throws ServerNotAvailableException
444
-	 */
445
-	public function getGroupGidNumber(string $dn) {
446
-		return $this->getEntryGidNumber($dn, 'gidNumber');
447
-	}
448
-
449
-	/**
450
-	 * returns the user's gidNumber
451
-	 *
452
-	 * @return string|bool
453
-	 * @throws ServerNotAvailableException
454
-	 */
455
-	public function getUserGidNumber(string $dn) {
456
-		$gidNumber = false;
457
-		if ($this->access->connection->hasGidNumber) {
458
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
459
-			if ($gidNumber === false) {
460
-				$this->access->connection->hasGidNumber = false;
461
-			}
462
-		}
463
-		return $gidNumber;
464
-	}
465
-
466
-	/**
467
-	 * @throws ServerNotAvailableException
468
-	 * @throws Exception
469
-	 */
470
-	private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
471
-		$groupID = $this->getGroupGidNumber($groupDN);
472
-		if ($groupID === false) {
473
-			throw new Exception('Not a valid group');
474
-		}
475
-
476
-		$filterParts = [];
477
-		$filterParts[] = $this->access->getFilterForUserCount();
478
-		if ($search !== '') {
479
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
480
-		}
481
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
482
-
483
-		return $this->access->combineFilterWithAnd($filterParts);
484
-	}
485
-
486
-	/**
487
-	 * returns a list of users that have the given group as gid number
488
-	 *
489
-	 * @throws ServerNotAvailableException
490
-	 */
491
-	public function getUsersInGidNumber(
492
-		string $groupDN,
493
-		string $search = '',
494
-		?int $limit = -1,
495
-		?int $offset = 0
496
-	): array {
497
-		try {
498
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
499
-			$users = $this->access->fetchListOfUsers(
500
-				$filter,
501
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
502
-				$limit,
503
-				$offset
504
-			);
505
-			return $this->access->nextcloudUserNames($users);
506
-		} catch (ServerNotAvailableException $e) {
507
-			throw $e;
508
-		} catch (Exception $e) {
509
-			return [];
510
-		}
511
-	}
512
-
513
-	/**
514
-	 * @throws ServerNotAvailableException
515
-	 * @return bool
516
-	 */
517
-	public function getUserGroupByGid(string $dn) {
518
-		$groupID = $this->getUserGidNumber($dn);
519
-		if ($groupID !== false) {
520
-			$groupName = $this->gidNumber2Name($groupID, $dn);
521
-			if ($groupName !== false) {
522
-				return $groupName;
523
-			}
524
-		}
525
-
526
-		return false;
527
-	}
528
-
529
-	/**
530
-	 * translates a primary group ID into an Nextcloud internal name
531
-	 *
532
-	 * @return string|bool
533
-	 * @throws Exception
534
-	 * @throws ServerNotAvailableException
535
-	 */
536
-	public function primaryGroupID2Name(string $gid, string $dn) {
537
-		$cacheKey = 'primaryGroupIDtoName';
538
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
539
-		if (!is_null($groupNames) && isset($groupNames[$gid])) {
540
-			return $groupNames[$gid];
541
-		}
542
-
543
-		$domainObjectSid = $this->access->getSID($dn);
544
-		if ($domainObjectSid === false) {
545
-			return false;
546
-		}
547
-
548
-		//we need to get the DN from LDAP
549
-		$filter = $this->access->combineFilterWithAnd([
550
-			$this->access->connection->ldapGroupFilter,
551
-			'objectsid=' . $domainObjectSid . '-' . $gid
552
-		]);
553
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
554
-	}
555
-
556
-	/**
557
-	 * returns the entry's primary group ID
558
-	 *
559
-	 * @return string|bool
560
-	 * @throws ServerNotAvailableException
561
-	 */
562
-	private function getEntryGroupID(string $dn, string $attribute) {
563
-		$value = $this->access->readAttribute($dn, $attribute);
564
-		if (is_array($value) && !empty($value)) {
565
-			return $value[0];
566
-		}
567
-		return false;
568
-	}
569
-
570
-	/**
571
-	 * @return string|bool
572
-	 * @throws ServerNotAvailableException
573
-	 */
574
-	public function getGroupPrimaryGroupID(string $dn) {
575
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
576
-	}
577
-
578
-	/**
579
-	 * @return string|bool
580
-	 * @throws ServerNotAvailableException
581
-	 */
582
-	public function getUserPrimaryGroupIDs(string $dn) {
583
-		$primaryGroupID = false;
584
-		if ($this->access->connection->hasPrimaryGroups) {
585
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
586
-			if ($primaryGroupID === false) {
587
-				$this->access->connection->hasPrimaryGroups = false;
588
-			}
589
-		}
590
-		return $primaryGroupID;
591
-	}
592
-
593
-	/**
594
-	 * @throws Exception
595
-	 * @throws ServerNotAvailableException
596
-	 */
597
-	private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
598
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
599
-		if ($groupID === false) {
600
-			throw new Exception('Not a valid group');
601
-		}
602
-
603
-		$filterParts = [];
604
-		$filterParts[] = $this->access->getFilterForUserCount();
605
-		if ($search !== '') {
606
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
607
-		}
608
-		$filterParts[] = 'primaryGroupID=' . $groupID;
609
-
610
-		return $this->access->combineFilterWithAnd($filterParts);
611
-	}
612
-
613
-	/**
614
-	 * @throws ServerNotAvailableException
615
-	 */
616
-	public function getUsersInPrimaryGroup(
617
-		string $groupDN,
618
-		string $search = '',
619
-		?int $limit = -1,
620
-		?int $offset = 0
621
-	): array {
622
-		try {
623
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
624
-			$users = $this->access->fetchListOfUsers(
625
-				$filter,
626
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
627
-				$limit,
628
-				$offset
629
-			);
630
-			return $this->access->nextcloudUserNames($users);
631
-		} catch (ServerNotAvailableException $e) {
632
-			throw $e;
633
-		} catch (Exception $e) {
634
-			return [];
635
-		}
636
-	}
637
-
638
-	/**
639
-	 * @throws ServerNotAvailableException
640
-	 */
641
-	public function countUsersInPrimaryGroup(
642
-		string $groupDN,
643
-		string $search = '',
644
-		int $limit = -1,
645
-		int $offset = 0
646
-	): int {
647
-		try {
648
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
649
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
650
-			return (int)$users;
651
-		} catch (ServerNotAvailableException $e) {
652
-			throw $e;
653
-		} catch (Exception $e) {
654
-			return 0;
655
-		}
656
-	}
657
-
658
-	/**
659
-	 * @return string|bool
660
-	 * @throws ServerNotAvailableException
661
-	 */
662
-	public function getUserPrimaryGroup(string $dn) {
663
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
664
-		if ($groupID !== false) {
665
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
666
-			if ($groupName !== false) {
667
-				return $groupName;
668
-			}
669
-		}
670
-
671
-		return false;
672
-	}
673
-
674
-	/**
675
-	 * This function fetches all groups a user belongs to. It does not check
676
-	 * if the user exists at all.
677
-	 *
678
-	 * This function includes groups based on dynamic group membership.
679
-	 *
680
-	 * @param string $uid Name of the user
681
-	 * @return array with group names
682
-	 * @throws Exception
683
-	 * @throws ServerNotAvailableException
684
-	 */
685
-	public function getUserGroups($uid) {
686
-		if (!$this->enabled) {
687
-			return [];
688
-		}
689
-		$cacheKey = 'getUserGroups' . $uid;
690
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
691
-		if (!is_null($userGroups)) {
692
-			return $userGroups;
693
-		}
694
-		$userDN = $this->access->username2dn($uid);
695
-		if (!$userDN) {
696
-			$this->access->connection->writeToCache($cacheKey, []);
697
-			return [];
698
-		}
699
-
700
-		$groups = [];
701
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
702
-		$gidGroupName = $this->getUserGroupByGid($userDN);
703
-
704
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
705
-
706
-		if (!empty($dynamicGroupMemberURL)) {
707
-			// look through dynamic groups to add them to the result array if needed
708
-			$groupsToMatch = $this->access->fetchListOfGroups(
709
-				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
710
-			foreach ($groupsToMatch as $dynamicGroup) {
711
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
712
-					continue;
713
-				}
714
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
715
-				if ($pos !== false) {
716
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
717
-					// apply filter via ldap search to see if this user is in this
718
-					// dynamic group
719
-					$userMatch = $this->access->readAttribute(
720
-						$userDN,
721
-						$this->access->connection->ldapUserDisplayName,
722
-						$memberUrlFilter
723
-					);
724
-					if ($userMatch !== false) {
725
-						// match found so this user is in this group
726
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
727
-						if (is_string($groupName)) {
728
-							// be sure to never return false if the dn could not be
729
-							// resolved to a name, for whatever reason.
730
-							$groups[] = $groupName;
731
-						}
732
-					}
733
-				} else {
734
-					$this->logger->debug('No search filter found on member url of group {dn}',
735
-						[
736
-							'app' => 'user_ldap',
737
-							'dn' => $dynamicGroup,
738
-						]
739
-					);
740
-				}
741
-			}
742
-		}
743
-
744
-		// if possible, read out membership via memberOf. It's far faster than
745
-		// performing a search, which still is a fallback later.
746
-		// memberof doesn't support memberuid, so skip it here.
747
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
748
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
749
-			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
750
-			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
751
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
752
-			if (is_array($groupDNs)) {
753
-				foreach ($groupDNs as $dn) {
754
-					$groupName = $this->access->dn2groupname($dn);
755
-					if (is_string($groupName)) {
756
-						// be sure to never return false if the dn could not be
757
-						// resolved to a name, for whatever reason.
758
-						$groups[] = $groupName;
759
-					}
760
-				}
761
-			}
762
-
763
-			if ($primaryGroup !== false) {
764
-				$groups[] = $primaryGroup;
765
-			}
766
-			if ($gidGroupName !== false) {
767
-				$groups[] = $gidGroupName;
768
-			}
769
-			$this->access->connection->writeToCache($cacheKey, $groups);
770
-			return $groups;
771
-		}
772
-
773
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
774
-		switch ($this->ldapGroupMemberAssocAttr) {
775
-			case 'uniquemember':
776
-			case 'member':
777
-				$uid = $userDN;
778
-				break;
779
-
780
-			case 'memberuid':
781
-			case 'zimbramailforwardingaddress':
782
-				$result = $this->access->readAttribute($userDN, 'uid');
783
-				if ($result === false) {
784
-					$this->logger->debug('No uid attribute found for DN {dn} on {host}',
785
-						[
786
-							'app' => 'user_ldap',
787
-							'dn' => $userDN,
788
-							'host' => $this->access->connection->ldapHost,
789
-						]
790
-					);
791
-					$uid = false;
792
-				} else {
793
-					$uid = $result[0];
794
-				}
795
-				break;
796
-
797
-			default:
798
-				// just in case
799
-				$uid = $userDN;
800
-				break;
801
-		}
802
-
803
-		if ($uid !== false) {
804
-			if (isset($this->cachedGroupsByMember[$uid])) {
805
-				$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
806
-			} else {
807
-				$groupsByMember = array_values($this->getGroupsByMember($uid));
808
-				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
809
-				$this->cachedGroupsByMember[$uid] = $groupsByMember;
810
-				$groups = array_merge($groups, $groupsByMember);
811
-			}
812
-		}
813
-
814
-		if ($primaryGroup !== false) {
815
-			$groups[] = $primaryGroup;
816
-		}
817
-		if ($gidGroupName !== false) {
818
-			$groups[] = $gidGroupName;
819
-		}
820
-
821
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
822
-		$this->access->connection->writeToCache($cacheKey, $groups);
823
-
824
-		return $groups;
825
-	}
826
-
827
-	/**
828
-	 * @throws ServerNotAvailableException
829
-	 */
830
-	private function getGroupsByMember(string $dn, array &$seen = null): array {
831
-		if ($seen === null) {
832
-			$seen = [];
833
-		}
834
-		if (array_key_exists($dn, $seen)) {
835
-			// avoid loops
836
-			return [];
837
-		}
838
-		$allGroups = [];
839
-		$seen[$dn] = true;
840
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
841
-
842
-		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
843
-			//in this case the member entries are email addresses
844
-			$filter .= '@*';
845
-		}
846
-
847
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
848
-		if ($nesting === 0) {
849
-			$filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
850
-		}
851
-
852
-		$groups = $this->access->fetchListOfGroups($filter,
853
-			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
854
-		if (is_array($groups)) {
855
-			$fetcher = function ($dn) use (&$seen) {
856
-				if (is_array($dn) && isset($dn['dn'][0])) {
857
-					$dn = $dn['dn'][0];
858
-				}
859
-				return $this->getGroupsByMember($dn, $seen);
860
-			};
861
-
862
-			if (empty($dn)) {
863
-				$dn = "";
864
-			}
865
-
866
-			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups, $seen);
867
-		}
868
-		$visibleGroups = $this->filterValidGroups($allGroups);
869
-		return array_intersect_key($allGroups, $visibleGroups);
870
-	}
871
-
872
-	/**
873
-	 * get a list of all users in a group
874
-	 *
875
-	 * @param string $gid
876
-	 * @param string $search
877
-	 * @param int $limit
878
-	 * @param int $offset
879
-	 * @return array with user ids
880
-	 * @throws Exception
881
-	 * @throws ServerNotAvailableException
882
-	 */
883
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
884
-		if (!$this->enabled) {
885
-			return [];
886
-		}
887
-		if (!$this->groupExists($gid)) {
888
-			return [];
889
-		}
890
-		$search = $this->access->escapeFilterPart($search, true);
891
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
892
-		// check for cache of the exact query
893
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
894
-		if (!is_null($groupUsers)) {
895
-			return $groupUsers;
896
-		}
897
-
898
-		if ($limit === -1) {
899
-			$limit = null;
900
-		}
901
-		// check for cache of the query without limit and offset
902
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
903
-		if (!is_null($groupUsers)) {
904
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
905
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
906
-			return $groupUsers;
907
-		}
908
-
909
-		$groupDN = $this->access->groupname2dn($gid);
910
-		if (!$groupDN) {
911
-			// group couldn't be found, return empty resultset
912
-			$this->access->connection->writeToCache($cacheKey, []);
913
-			return [];
914
-		}
915
-
916
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
917
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
918
-		$members = $this->_groupMembers($groupDN);
919
-		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
920
-			//in case users could not be retrieved, return empty result set
921
-			$this->access->connection->writeToCache($cacheKey, []);
922
-			return [];
923
-		}
924
-
925
-		$groupUsers = [];
926
-		$attrs = $this->access->userManager->getAttributes(true);
927
-		foreach ($members as $member) {
928
-			switch ($this->ldapGroupMemberAssocAttr) {
929
-				/** @noinspection PhpMissingBreakStatementInspection */
930
-				case 'zimbramailforwardingaddress':
931
-					//we get email addresses and need to convert them to uids
932
-					$parts = explode('@', $member);
933
-					$member = $parts[0];
934
-					//no break needed because we just needed to remove the email part and now we have uids
935
-				case 'memberuid':
936
-					//we got uids, need to get their DNs to 'translate' them to user names
937
-					$filter = $this->access->combineFilterWithAnd([
938
-						str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
939
-						$this->access->combineFilterWithAnd([
940
-							$this->access->getFilterPartForUserSearch($search),
941
-							$this->access->connection->ldapUserFilter
942
-						])
943
-					]);
944
-					$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
945
-					if (empty($ldap_users)) {
946
-						break;
947
-					}
948
-					$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
949
-					break;
950
-				default:
951
-					//we got DNs, check if we need to filter by search or we can give back all of them
952
-					$uid = $this->access->dn2username($member);
953
-					if (!$uid) {
954
-						break;
955
-					}
956
-
957
-					$cacheKey = 'userExistsOnLDAP' . $uid;
958
-					$userExists = $this->access->connection->getFromCache($cacheKey);
959
-					if ($userExists === false) {
960
-						break;
961
-					}
962
-					if ($userExists === null || $search !== '') {
963
-						if (!$this->access->readAttribute($member,
964
-							$this->access->connection->ldapUserDisplayName,
965
-							$this->access->combineFilterWithAnd([
966
-								$this->access->getFilterPartForUserSearch($search),
967
-								$this->access->connection->ldapUserFilter
968
-							]))) {
969
-							if ($search === '') {
970
-								$this->access->connection->writeToCache($cacheKey, false);
971
-							}
972
-							break;
973
-						}
974
-						$this->access->connection->writeToCache($cacheKey, true);
975
-					}
976
-					$groupUsers[] = $uid;
977
-					break;
978
-			}
979
-		}
980
-
981
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
982
-		natsort($groupUsers);
983
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
984
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
985
-
986
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
987
-
988
-		return $groupUsers;
989
-	}
990
-
991
-	/**
992
-	 * returns the number of users in a group, who match the search term
993
-	 *
994
-	 * @param string $gid the internal group name
995
-	 * @param string $search optional, a search string
996
-	 * @return int|bool
997
-	 * @throws Exception
998
-	 * @throws ServerNotAvailableException
999
-	 */
1000
-	public function countUsersInGroup($gid, $search = '') {
1001
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
1002
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
1003
-		}
1004
-
1005
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
1006
-		if (!$this->enabled || !$this->groupExists($gid)) {
1007
-			return false;
1008
-		}
1009
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
1010
-		if (!is_null($groupUsers)) {
1011
-			return $groupUsers;
1012
-		}
1013
-
1014
-		$groupDN = $this->access->groupname2dn($gid);
1015
-		if (!$groupDN) {
1016
-			// group couldn't be found, return empty result set
1017
-			$this->access->connection->writeToCache($cacheKey, false);
1018
-			return false;
1019
-		}
1020
-
1021
-		$members = $this->_groupMembers($groupDN);
1022
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
1023
-		if (!$members && $primaryUserCount === 0) {
1024
-			//in case users could not be retrieved, return empty result set
1025
-			$this->access->connection->writeToCache($cacheKey, false);
1026
-			return false;
1027
-		}
1028
-
1029
-		if ($search === '') {
1030
-			$groupUsers = count($members) + $primaryUserCount;
1031
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
1032
-			return $groupUsers;
1033
-		}
1034
-		$search = $this->access->escapeFilterPart($search, true);
1035
-		$isMemberUid =
1036
-			($this->ldapGroupMemberAssocAttr === 'memberuid' ||
1037
-				$this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
1038
-
1039
-		//we need to apply the search filter
1040
-		//alternatives that need to be checked:
1041
-		//a) get all users by search filter and array_intersect them
1042
-		//b) a, but only when less than 1k 10k ?k users like it is
1043
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
1044
-		//   and let it count.
1045
-		//For now this is not important, because the only use of this method
1046
-		//does not supply a search string
1047
-		$groupUsers = [];
1048
-		foreach ($members as $member) {
1049
-			if ($isMemberUid) {
1050
-				if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
1051
-					//we get email addresses and need to convert them to uids
1052
-					$parts = explode('@', $member);
1053
-					$member = $parts[0];
1054
-				}
1055
-				//we got uids, need to get their DNs to 'translate' them to user names
1056
-				$filter = $this->access->combineFilterWithAnd([
1057
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
1058
-					$this->access->getFilterPartForUserSearch($search)
1059
-				]);
1060
-				$ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
1061
-				if (count($ldap_users) < 1) {
1062
-					continue;
1063
-				}
1064
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
1065
-			} else {
1066
-				//we need to apply the search filter now
1067
-				if (!$this->access->readAttribute($member,
1068
-					$this->access->connection->ldapUserDisplayName,
1069
-					$this->access->getFilterPartForUserSearch($search))) {
1070
-					continue;
1071
-				}
1072
-				// dn2username will also check if the users belong to the allowed base
1073
-				if ($ncGroupId = $this->access->dn2username($member)) {
1074
-					$groupUsers[] = $ncGroupId;
1075
-				}
1076
-			}
1077
-		}
1078
-
1079
-		//and get users that have the group as primary
1080
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1081
-
1082
-		return count($groupUsers) + $primaryUsers;
1083
-	}
1084
-
1085
-	/**
1086
-	 * get a list of all groups using a paged search
1087
-	 *
1088
-	 * @param string $search
1089
-	 * @param int $limit
1090
-	 * @param int $offset
1091
-	 * @return array with group names
1092
-	 *
1093
-	 * Returns a list with all groups
1094
-	 * Uses a paged search if available to override a
1095
-	 * server side search limit.
1096
-	 * (active directory has a limit of 1000 by default)
1097
-	 * @throws Exception
1098
-	 */
1099
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1100
-		if (!$this->enabled) {
1101
-			return [];
1102
-		}
1103
-		$search = $this->access->escapeFilterPart($search, true);
1104
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1105
-
1106
-		//Check cache before driving unnecessary searches
1107
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
1108
-		if (!is_null($ldap_groups)) {
1109
-			return $ldap_groups;
1110
-		}
1111
-
1112
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1113
-		// error. With a limit of 0, we get 0 results. So we pass null.
1114
-		if ($limit <= 0) {
1115
-			$limit = null;
1116
-		}
1117
-		$filter = $this->access->combineFilterWithAnd([
1118
-			$this->access->connection->ldapGroupFilter,
1119
-			$this->access->getFilterPartForGroupSearch($search)
1120
-		]);
1121
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
1122
-			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1123
-			$limit,
1124
-			$offset);
1125
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1126
-
1127
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1128
-		return $ldap_groups;
1129
-	}
1130
-
1131
-	/**
1132
-	 * check if a group exists
1133
-	 *
1134
-	 * @param string $gid
1135
-	 * @return bool
1136
-	 * @throws ServerNotAvailableException
1137
-	 */
1138
-	public function groupExists($gid) {
1139
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1140
-		if (!is_null($groupExists)) {
1141
-			return (bool)$groupExists;
1142
-		}
1143
-
1144
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1145
-		//only, requires more checking.
1146
-		$dn = $this->access->groupname2dn($gid);
1147
-		if (!$dn) {
1148
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1149
-			return false;
1150
-		}
1151
-
1152
-		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1153
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1154
-			return false;
1155
-		}
1156
-
1157
-		//if group really still exists, we will be able to read its objectClass
1158
-		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1159
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1160
-			return false;
1161
-		}
1162
-
1163
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1164
-		return true;
1165
-	}
1166
-
1167
-	/**
1168
-	 * @throws ServerNotAvailableException
1169
-	 * @throws Exception
1170
-	 */
1171
-	protected function filterValidGroups(array $listOfGroups): array {
1172
-		$validGroupDNs = [];
1173
-		foreach ($listOfGroups as $key => $item) {
1174
-			$dn = is_string($item) ? $item : $item['dn'][0];
1175
-			$gid = $this->access->dn2groupname($dn);
1176
-			if (!$gid) {
1177
-				continue;
1178
-			}
1179
-			if ($this->groupExists($gid)) {
1180
-				$validGroupDNs[$key] = $item;
1181
-			}
1182
-		}
1183
-		return $validGroupDNs;
1184
-	}
1185
-
1186
-	/**
1187
-	 * Check if backend implements actions
1188
-	 *
1189
-	 * @param int $actions bitwise-or'ed actions
1190
-	 * @return boolean
1191
-	 *
1192
-	 * Returns the supported actions as int to be
1193
-	 * compared with GroupInterface::CREATE_GROUP etc.
1194
-	 */
1195
-	public function implementsActions($actions) {
1196
-		return (bool)((GroupInterface::COUNT_USERS |
1197
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1198
-	}
1199
-
1200
-	/**
1201
-	 * Return access for LDAP interaction.
1202
-	 *
1203
-	 * @return Access instance of Access for LDAP interaction
1204
-	 */
1205
-	public function getLDAPAccess($gid) {
1206
-		return $this->access;
1207
-	}
1208
-
1209
-	/**
1210
-	 * create a group
1211
-	 *
1212
-	 * @param string $gid
1213
-	 * @return bool
1214
-	 * @throws Exception
1215
-	 * @throws ServerNotAvailableException
1216
-	 */
1217
-	public function createGroup($gid) {
1218
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1219
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1220
-				//updates group mapping
1221
-				$uuid = $this->access->getUUID($dn, false);
1222
-				if (is_string($uuid)) {
1223
-					$this->access->mapAndAnnounceIfApplicable(
1224
-						$this->access->getGroupMapper(),
1225
-						$dn,
1226
-						$gid,
1227
-						$uuid,
1228
-						false
1229
-					);
1230
-					$this->access->cacheGroupExists($gid);
1231
-				}
1232
-			}
1233
-			return $dn != null;
1234
-		}
1235
-		throw new Exception('Could not create group in LDAP backend.');
1236
-	}
1237
-
1238
-	/**
1239
-	 * delete a group
1240
-	 *
1241
-	 * @param string $gid gid of the group to delete
1242
-	 * @return bool
1243
-	 * @throws Exception
1244
-	 */
1245
-	public function deleteGroup($gid) {
1246
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1247
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1248
-				#delete group in nextcloud internal db
1249
-				$this->access->getGroupMapper()->unmap($gid);
1250
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1251
-			}
1252
-			return $ret;
1253
-		}
1254
-		throw new Exception('Could not delete group in LDAP backend.');
1255
-	}
1256
-
1257
-	/**
1258
-	 * Add a user to a group
1259
-	 *
1260
-	 * @param string $uid Name of the user to add to group
1261
-	 * @param string $gid Name of the group in which add the user
1262
-	 * @return bool
1263
-	 * @throws Exception
1264
-	 */
1265
-	public function addToGroup($uid, $gid) {
1266
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1267
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1268
-				$this->access->connection->clearCache();
1269
-				unset($this->cachedGroupMembers[$gid]);
1270
-			}
1271
-			return $ret;
1272
-		}
1273
-		throw new Exception('Could not add user to group in LDAP backend.');
1274
-	}
1275
-
1276
-	/**
1277
-	 * Removes a user from a group
1278
-	 *
1279
-	 * @param string $uid Name of the user to remove from group
1280
-	 * @param string $gid Name of the group from which remove the user
1281
-	 * @return bool
1282
-	 * @throws Exception
1283
-	 */
1284
-	public function removeFromGroup($uid, $gid) {
1285
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1286
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1287
-				$this->access->connection->clearCache();
1288
-				unset($this->cachedGroupMembers[$gid]);
1289
-			}
1290
-			return $ret;
1291
-		}
1292
-		throw new Exception('Could not remove user from group in LDAP backend.');
1293
-	}
1294
-
1295
-	/**
1296
-	 * Gets group details
1297
-	 *
1298
-	 * @param string $gid Name of the group
1299
-	 * @return array|false
1300
-	 * @throws Exception
1301
-	 */
1302
-	public function getGroupDetails($gid) {
1303
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1304
-			return $this->groupPluginManager->getGroupDetails($gid);
1305
-		}
1306
-		throw new Exception('Could not get group details in LDAP backend.');
1307
-	}
1308
-
1309
-	/**
1310
-	 * Return LDAP connection resource from a cloned connection.
1311
-	 * The cloned connection needs to be closed manually.
1312
-	 * of the current access.
1313
-	 *
1314
-	 * @param string $gid
1315
-	 * @return resource of the LDAP connection
1316
-	 * @throws ServerNotAvailableException
1317
-	 */
1318
-	public function getNewLDAPConnection($gid) {
1319
-		$connection = clone $this->access->getConnection();
1320
-		return $connection->getConnectionResource();
1321
-	}
1322
-
1323
-	/**
1324
-	 * @throws ServerNotAvailableException
1325
-	 */
1326
-	public function getDisplayName(string $gid): string {
1327
-		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1328
-			return $this->groupPluginManager->getDisplayName($gid);
1329
-		}
1330
-
1331
-		$cacheKey = 'group_getDisplayName' . $gid;
1332
-		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1333
-			return $displayName;
1334
-		}
1335
-
1336
-		$displayName = $this->access->readAttribute(
1337
-			$this->access->groupname2dn($gid),
1338
-			$this->access->connection->ldapGroupDisplayName);
1339
-
1340
-		if ($displayName && (count($displayName) > 0)) {
1341
-			$displayName = $displayName[0];
1342
-			$this->access->connection->writeToCache($cacheKey, $displayName);
1343
-			return $displayName;
1344
-		}
1345
-
1346
-		return '';
1347
-	}
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
+
140
+        //extra work if we don't get back user DNs
141
+        switch ($this->ldapGroupMemberAssocAttr) {
142
+            case 'memberuid':
143
+            case 'zimbramailforwardingaddress':
144
+                $requestAttributes = $this->access->userManager->getAttributes(true);
145
+                $users = [];
146
+                $filterParts = [];
147
+                $bytes = 0;
148
+                foreach ($members as $mid) {
149
+                    if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
150
+                        $parts = explode('@', $mid); //making sure we get only the uid
151
+                        $mid = $parts[0];
152
+                    }
153
+                    $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
154
+                    $filterParts[] = $filter;
155
+                    $bytes += strlen($filter);
156
+                    if ($bytes >= 9000000) {
157
+                        // AD has a default input buffer of 10 MB, we do not want
158
+                        // to take even the chance to exceed it
159
+                        // so we fetch results with the filterParts we collected so far
160
+                        $filter = $this->access->combineFilterWithOr($filterParts);
161
+                        $search = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
162
+                        $bytes = 0;
163
+                        $filterParts = [];
164
+                        $users = array_merge($users, $search);
165
+                    }
166
+                }
167
+
168
+                if (count($filterParts) > 0) {
169
+                    // if there are filterParts left we need to add their result
170
+                    $filter = $this->access->combineFilterWithOr($filterParts);
171
+                    $search = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
172
+                    $users = array_merge($users, $search);
173
+                }
174
+
175
+                // now we cleanup the users array to get only dns
176
+                $dns = [];
177
+                foreach ($users as $record) {
178
+                    $dns[$record['dn'][0]] = 1;
179
+                }
180
+                $members = array_keys($dns);
181
+
182
+                break;
183
+        }
184
+
185
+        if (count($members) === 0) {
186
+            $this->access->connection->writeToCache($cacheKey, false);
187
+            return false;
188
+        }
189
+
190
+        $isInGroup = in_array($userDN, $members);
191
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
192
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
193
+        $this->cachedGroupMembers[$gid] = $members;
194
+
195
+        return $isInGroup;
196
+    }
197
+
198
+    /**
199
+     * For a group that has user membership defined by an LDAP search url
200
+     * attribute returns the users that match the search url otherwise returns
201
+     * an empty array.
202
+     *
203
+     * @throws ServerNotAvailableException
204
+     */
205
+    public function getDynamicGroupMembers(string $dnGroup): array {
206
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
207
+
208
+        if (empty($dynamicGroupMemberURL)) {
209
+            return [];
210
+        }
211
+
212
+        $dynamicMembers = [];
213
+        $memberURLs = $this->access->readAttribute(
214
+            $dnGroup,
215
+            $dynamicGroupMemberURL,
216
+            $this->access->connection->ldapGroupFilter
217
+        );
218
+        if ($memberURLs !== false) {
219
+            // this group has the 'memberURL' attribute so this is a dynamic group
220
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
221
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
222
+            $pos = strpos($memberURLs[0], '(');
223
+            if ($pos !== false) {
224
+                $memberUrlFilter = substr($memberURLs[0], $pos);
225
+                $foundMembers = $this->access->searchUsers($memberUrlFilter, ['dn']);
226
+                $dynamicMembers = [];
227
+                foreach ($foundMembers as $value) {
228
+                    $dynamicMembers[$value['dn'][0]] = 1;
229
+                }
230
+            } else {
231
+                $this->logger->debug('No search filter found on member url of group {dn}',
232
+                    [
233
+                        'app' => 'user_ldap',
234
+                        'dn' => $dnGroup,
235
+                    ]
236
+                );
237
+            }
238
+        }
239
+        return $dynamicMembers;
240
+    }
241
+
242
+    /**
243
+     * @throws ServerNotAvailableException
244
+     */
245
+    private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
246
+        if ($seen === null) {
247
+            $seen = [];
248
+            // the root entry has to be marked as processed to avoind infinit loops,
249
+            // but not included in the results laters on
250
+            $excludeFromResult = $dnGroup;
251
+        }
252
+        $allMembers = [];
253
+        if (array_key_exists($dnGroup, $seen)) {
254
+            return [];
255
+        }
256
+        // used extensively in cron job, caching makes sense for nested groups
257
+        $cacheKey = '_groupMembers' . $dnGroup;
258
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
259
+        if ($groupMembers !== null) {
260
+            return $groupMembers;
261
+        }
262
+
263
+        if ($this->access->connection->ldapNestedGroups
264
+            && $this->access->connection->useMemberOfToDetectMembership
265
+            && $this->access->connection->hasMemberOfFilterSupport
266
+            && $this->access->connection->ldapMatchingRuleInChainState !== Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE
267
+        ) {
268
+            $attemptedLdapMatchingRuleInChain = true;
269
+            // compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
270
+            $filter = $this->access->combineFilterWithAnd([
271
+                $this->access->connection->ldapUserFilter,
272
+                $this->access->connection->ldapUserDisplayName . '=*',
273
+                'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
274
+            ]);
275
+            $memberRecords = $this->access->fetchListOfUsers(
276
+                $filter,
277
+                $this->access->userManager->getAttributes(true)
278
+            );
279
+            $result = array_reduce($memberRecords, function ($carry, $record) {
280
+                $carry[] = $record['dn'][0];
281
+                return $carry;
282
+            }, []);
283
+            if ($this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_AVAILABLE) {
284
+                return $result;
285
+            } elseif (!empty($memberRecords)) {
286
+                $this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_AVAILABLE;
287
+                $this->access->connection->saveConfiguration();
288
+                return $result;
289
+            }
290
+            // when feature availability is unknown, and the result is empty, continue and test with original approach
291
+        }
292
+
293
+        $seen[$dnGroup] = 1;
294
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
295
+        if (is_array($members)) {
296
+            $fetcher = function ($memberDN) use (&$seen) {
297
+                return $this->_groupMembers($memberDN, $seen);
298
+            };
299
+            $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members, $seen);
300
+        }
301
+
302
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
303
+        if (isset($excludeFromResult)) {
304
+            $index = array_search($excludeFromResult, $allMembers, true);
305
+            if ($index !== false) {
306
+                unset($allMembers[$index]);
307
+            }
308
+        }
309
+
310
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
311
+        if (isset($attemptedLdapMatchingRuleInChain)
312
+            && $this->access->connection->ldapMatchingRuleInChainState === Configuration::LDAP_SERVER_FEATURE_UNKNOWN
313
+            && !empty($allMembers)
314
+        ) {
315
+            $this->access->connection->ldapMatchingRuleInChainState = Configuration::LDAP_SERVER_FEATURE_UNAVAILABLE;
316
+            $this->access->connection->saveConfiguration();
317
+        }
318
+        return $allMembers;
319
+    }
320
+
321
+    /**
322
+     * @throws ServerNotAvailableException
323
+     */
324
+    private function _getGroupDNsFromMemberOf(string $dn): array {
325
+        $groups = $this->access->readAttribute($dn, 'memberOf');
326
+        if (!is_array($groups)) {
327
+            return [];
328
+        }
329
+
330
+        $fetcher = function ($groupDN) {
331
+            if (isset($this->cachedNestedGroups[$groupDN])) {
332
+                $nestedGroups = $this->cachedNestedGroups[$groupDN];
333
+            } else {
334
+                $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
335
+                if (!is_array($nestedGroups)) {
336
+                    $nestedGroups = [];
337
+                }
338
+                $this->cachedNestedGroups[$groupDN] = $nestedGroups;
339
+            }
340
+            return $nestedGroups;
341
+        };
342
+
343
+        $groups = $this->walkNestedGroups($dn, $fetcher, $groups);
344
+        return $this->filterValidGroups($groups);
345
+    }
346
+
347
+    private function walkNestedGroups(string $dn, Closure $fetcher, array $list, array &$seen = []): array {
348
+        $nesting = (int)$this->access->connection->ldapNestedGroups;
349
+        // depending on the input, we either have a list of DNs or a list of LDAP records
350
+        // also, the output expects either DNs or records. Testing the first element should suffice.
351
+        $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
352
+
353
+        if ($nesting !== 1) {
354
+            if ($recordMode) {
355
+                // the keys are numeric, but should hold the DN
356
+                return array_reduce($list, function ($transformed, $record) use ($dn) {
357
+                    if ($record['dn'][0] != $dn) {
358
+                        $transformed[$record['dn'][0]] = $record;
359
+                    }
360
+                    return $transformed;
361
+                }, []);
362
+            }
363
+            return $list;
364
+        }
365
+
366
+        while ($record = array_shift($list)) {
367
+            $recordDN = $record['dn'][0] ?? $record;
368
+            if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
369
+                // Prevent loops
370
+                continue;
371
+            }
372
+            $fetched = $fetcher($record);
373
+            $list = array_merge($list, $fetched);
374
+            if (!isset($seen[$recordDN]) || is_bool($seen[$recordDN]) && is_array($record)) {
375
+                $seen[$recordDN] = $record;
376
+            }
377
+        }
378
+
379
+        // on record mode, filter out intermediate state
380
+        return $recordMode ? array_filter($seen, 'is_array') : array_keys($seen);
381
+    }
382
+
383
+    /**
384
+     * translates a gidNumber into an ownCloud internal name
385
+     *
386
+     * @return string|bool
387
+     * @throws Exception
388
+     * @throws ServerNotAvailableException
389
+     */
390
+    public function gidNumber2Name(string $gid, string $dn) {
391
+        $cacheKey = 'gidNumberToName' . $gid;
392
+        $groupName = $this->access->connection->getFromCache($cacheKey);
393
+        if (!is_null($groupName) && isset($groupName)) {
394
+            return $groupName;
395
+        }
396
+
397
+        //we need to get the DN from LDAP
398
+        $filter = $this->access->combineFilterWithAnd([
399
+            $this->access->connection->ldapGroupFilter,
400
+            'objectClass=posixGroup',
401
+            $this->access->connection->ldapGidNumber . '=' . $gid
402
+        ]);
403
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
404
+    }
405
+
406
+    /**
407
+     * @throws ServerNotAvailableException
408
+     * @throws Exception
409
+     */
410
+    private function getNameOfGroup(string $filter, string $cacheKey) {
411
+        $result = $this->access->searchGroups($filter, ['dn'], 1);
412
+        if (empty($result)) {
413
+            return null;
414
+        }
415
+        $dn = $result[0]['dn'][0];
416
+
417
+        //and now the group name
418
+        //NOTE once we have separate Nextcloud group IDs and group names we can
419
+        //directly read the display name attribute instead of the DN
420
+        $name = $this->access->dn2groupname($dn);
421
+
422
+        $this->access->connection->writeToCache($cacheKey, $name);
423
+
424
+        return $name;
425
+    }
426
+
427
+    /**
428
+     * returns the entry's gidNumber
429
+     *
430
+     * @return string|bool
431
+     * @throws ServerNotAvailableException
432
+     */
433
+    private function getEntryGidNumber(string $dn, string $attribute) {
434
+        $value = $this->access->readAttribute($dn, $attribute);
435
+        if (is_array($value) && !empty($value)) {
436
+            return $value[0];
437
+        }
438
+        return false;
439
+    }
440
+
441
+    /**
442
+     * @return string|bool
443
+     * @throws ServerNotAvailableException
444
+     */
445
+    public function getGroupGidNumber(string $dn) {
446
+        return $this->getEntryGidNumber($dn, 'gidNumber');
447
+    }
448
+
449
+    /**
450
+     * returns the user's gidNumber
451
+     *
452
+     * @return string|bool
453
+     * @throws ServerNotAvailableException
454
+     */
455
+    public function getUserGidNumber(string $dn) {
456
+        $gidNumber = false;
457
+        if ($this->access->connection->hasGidNumber) {
458
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
459
+            if ($gidNumber === false) {
460
+                $this->access->connection->hasGidNumber = false;
461
+            }
462
+        }
463
+        return $gidNumber;
464
+    }
465
+
466
+    /**
467
+     * @throws ServerNotAvailableException
468
+     * @throws Exception
469
+     */
470
+    private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
471
+        $groupID = $this->getGroupGidNumber($groupDN);
472
+        if ($groupID === false) {
473
+            throw new Exception('Not a valid group');
474
+        }
475
+
476
+        $filterParts = [];
477
+        $filterParts[] = $this->access->getFilterForUserCount();
478
+        if ($search !== '') {
479
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
480
+        }
481
+        $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
482
+
483
+        return $this->access->combineFilterWithAnd($filterParts);
484
+    }
485
+
486
+    /**
487
+     * returns a list of users that have the given group as gid number
488
+     *
489
+     * @throws ServerNotAvailableException
490
+     */
491
+    public function getUsersInGidNumber(
492
+        string $groupDN,
493
+        string $search = '',
494
+        ?int $limit = -1,
495
+        ?int $offset = 0
496
+    ): array {
497
+        try {
498
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
499
+            $users = $this->access->fetchListOfUsers(
500
+                $filter,
501
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
502
+                $limit,
503
+                $offset
504
+            );
505
+            return $this->access->nextcloudUserNames($users);
506
+        } catch (ServerNotAvailableException $e) {
507
+            throw $e;
508
+        } catch (Exception $e) {
509
+            return [];
510
+        }
511
+    }
512
+
513
+    /**
514
+     * @throws ServerNotAvailableException
515
+     * @return bool
516
+     */
517
+    public function getUserGroupByGid(string $dn) {
518
+        $groupID = $this->getUserGidNumber($dn);
519
+        if ($groupID !== false) {
520
+            $groupName = $this->gidNumber2Name($groupID, $dn);
521
+            if ($groupName !== false) {
522
+                return $groupName;
523
+            }
524
+        }
525
+
526
+        return false;
527
+    }
528
+
529
+    /**
530
+     * translates a primary group ID into an Nextcloud internal name
531
+     *
532
+     * @return string|bool
533
+     * @throws Exception
534
+     * @throws ServerNotAvailableException
535
+     */
536
+    public function primaryGroupID2Name(string $gid, string $dn) {
537
+        $cacheKey = 'primaryGroupIDtoName';
538
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
539
+        if (!is_null($groupNames) && isset($groupNames[$gid])) {
540
+            return $groupNames[$gid];
541
+        }
542
+
543
+        $domainObjectSid = $this->access->getSID($dn);
544
+        if ($domainObjectSid === false) {
545
+            return false;
546
+        }
547
+
548
+        //we need to get the DN from LDAP
549
+        $filter = $this->access->combineFilterWithAnd([
550
+            $this->access->connection->ldapGroupFilter,
551
+            'objectsid=' . $domainObjectSid . '-' . $gid
552
+        ]);
553
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
554
+    }
555
+
556
+    /**
557
+     * returns the entry's primary group ID
558
+     *
559
+     * @return string|bool
560
+     * @throws ServerNotAvailableException
561
+     */
562
+    private function getEntryGroupID(string $dn, string $attribute) {
563
+        $value = $this->access->readAttribute($dn, $attribute);
564
+        if (is_array($value) && !empty($value)) {
565
+            return $value[0];
566
+        }
567
+        return false;
568
+    }
569
+
570
+    /**
571
+     * @return string|bool
572
+     * @throws ServerNotAvailableException
573
+     */
574
+    public function getGroupPrimaryGroupID(string $dn) {
575
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
576
+    }
577
+
578
+    /**
579
+     * @return string|bool
580
+     * @throws ServerNotAvailableException
581
+     */
582
+    public function getUserPrimaryGroupIDs(string $dn) {
583
+        $primaryGroupID = false;
584
+        if ($this->access->connection->hasPrimaryGroups) {
585
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
586
+            if ($primaryGroupID === false) {
587
+                $this->access->connection->hasPrimaryGroups = false;
588
+            }
589
+        }
590
+        return $primaryGroupID;
591
+    }
592
+
593
+    /**
594
+     * @throws Exception
595
+     * @throws ServerNotAvailableException
596
+     */
597
+    private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
598
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
599
+        if ($groupID === false) {
600
+            throw new Exception('Not a valid group');
601
+        }
602
+
603
+        $filterParts = [];
604
+        $filterParts[] = $this->access->getFilterForUserCount();
605
+        if ($search !== '') {
606
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
607
+        }
608
+        $filterParts[] = 'primaryGroupID=' . $groupID;
609
+
610
+        return $this->access->combineFilterWithAnd($filterParts);
611
+    }
612
+
613
+    /**
614
+     * @throws ServerNotAvailableException
615
+     */
616
+    public function getUsersInPrimaryGroup(
617
+        string $groupDN,
618
+        string $search = '',
619
+        ?int $limit = -1,
620
+        ?int $offset = 0
621
+    ): array {
622
+        try {
623
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
624
+            $users = $this->access->fetchListOfUsers(
625
+                $filter,
626
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
627
+                $limit,
628
+                $offset
629
+            );
630
+            return $this->access->nextcloudUserNames($users);
631
+        } catch (ServerNotAvailableException $e) {
632
+            throw $e;
633
+        } catch (Exception $e) {
634
+            return [];
635
+        }
636
+    }
637
+
638
+    /**
639
+     * @throws ServerNotAvailableException
640
+     */
641
+    public function countUsersInPrimaryGroup(
642
+        string $groupDN,
643
+        string $search = '',
644
+        int $limit = -1,
645
+        int $offset = 0
646
+    ): int {
647
+        try {
648
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
649
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
650
+            return (int)$users;
651
+        } catch (ServerNotAvailableException $e) {
652
+            throw $e;
653
+        } catch (Exception $e) {
654
+            return 0;
655
+        }
656
+    }
657
+
658
+    /**
659
+     * @return string|bool
660
+     * @throws ServerNotAvailableException
661
+     */
662
+    public function getUserPrimaryGroup(string $dn) {
663
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
664
+        if ($groupID !== false) {
665
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
666
+            if ($groupName !== false) {
667
+                return $groupName;
668
+            }
669
+        }
670
+
671
+        return false;
672
+    }
673
+
674
+    /**
675
+     * This function fetches all groups a user belongs to. It does not check
676
+     * if the user exists at all.
677
+     *
678
+     * This function includes groups based on dynamic group membership.
679
+     *
680
+     * @param string $uid Name of the user
681
+     * @return array with group names
682
+     * @throws Exception
683
+     * @throws ServerNotAvailableException
684
+     */
685
+    public function getUserGroups($uid) {
686
+        if (!$this->enabled) {
687
+            return [];
688
+        }
689
+        $cacheKey = 'getUserGroups' . $uid;
690
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
691
+        if (!is_null($userGroups)) {
692
+            return $userGroups;
693
+        }
694
+        $userDN = $this->access->username2dn($uid);
695
+        if (!$userDN) {
696
+            $this->access->connection->writeToCache($cacheKey, []);
697
+            return [];
698
+        }
699
+
700
+        $groups = [];
701
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
702
+        $gidGroupName = $this->getUserGroupByGid($userDN);
703
+
704
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
705
+
706
+        if (!empty($dynamicGroupMemberURL)) {
707
+            // look through dynamic groups to add them to the result array if needed
708
+            $groupsToMatch = $this->access->fetchListOfGroups(
709
+                $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
710
+            foreach ($groupsToMatch as $dynamicGroup) {
711
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
712
+                    continue;
713
+                }
714
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
715
+                if ($pos !== false) {
716
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
717
+                    // apply filter via ldap search to see if this user is in this
718
+                    // dynamic group
719
+                    $userMatch = $this->access->readAttribute(
720
+                        $userDN,
721
+                        $this->access->connection->ldapUserDisplayName,
722
+                        $memberUrlFilter
723
+                    );
724
+                    if ($userMatch !== false) {
725
+                        // match found so this user is in this group
726
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
727
+                        if (is_string($groupName)) {
728
+                            // be sure to never return false if the dn could not be
729
+                            // resolved to a name, for whatever reason.
730
+                            $groups[] = $groupName;
731
+                        }
732
+                    }
733
+                } else {
734
+                    $this->logger->debug('No search filter found on member url of group {dn}',
735
+                        [
736
+                            'app' => 'user_ldap',
737
+                            'dn' => $dynamicGroup,
738
+                        ]
739
+                    );
740
+                }
741
+            }
742
+        }
743
+
744
+        // if possible, read out membership via memberOf. It's far faster than
745
+        // performing a search, which still is a fallback later.
746
+        // memberof doesn't support memberuid, so skip it here.
747
+        if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
748
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
749
+            && $this->ldapGroupMemberAssocAttr !== 'memberuid'
750
+            && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
751
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
752
+            if (is_array($groupDNs)) {
753
+                foreach ($groupDNs as $dn) {
754
+                    $groupName = $this->access->dn2groupname($dn);
755
+                    if (is_string($groupName)) {
756
+                        // be sure to never return false if the dn could not be
757
+                        // resolved to a name, for whatever reason.
758
+                        $groups[] = $groupName;
759
+                    }
760
+                }
761
+            }
762
+
763
+            if ($primaryGroup !== false) {
764
+                $groups[] = $primaryGroup;
765
+            }
766
+            if ($gidGroupName !== false) {
767
+                $groups[] = $gidGroupName;
768
+            }
769
+            $this->access->connection->writeToCache($cacheKey, $groups);
770
+            return $groups;
771
+        }
772
+
773
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
774
+        switch ($this->ldapGroupMemberAssocAttr) {
775
+            case 'uniquemember':
776
+            case 'member':
777
+                $uid = $userDN;
778
+                break;
779
+
780
+            case 'memberuid':
781
+            case 'zimbramailforwardingaddress':
782
+                $result = $this->access->readAttribute($userDN, 'uid');
783
+                if ($result === false) {
784
+                    $this->logger->debug('No uid attribute found for DN {dn} on {host}',
785
+                        [
786
+                            'app' => 'user_ldap',
787
+                            'dn' => $userDN,
788
+                            'host' => $this->access->connection->ldapHost,
789
+                        ]
790
+                    );
791
+                    $uid = false;
792
+                } else {
793
+                    $uid = $result[0];
794
+                }
795
+                break;
796
+
797
+            default:
798
+                // just in case
799
+                $uid = $userDN;
800
+                break;
801
+        }
802
+
803
+        if ($uid !== false) {
804
+            if (isset($this->cachedGroupsByMember[$uid])) {
805
+                $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
806
+            } else {
807
+                $groupsByMember = array_values($this->getGroupsByMember($uid));
808
+                $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
809
+                $this->cachedGroupsByMember[$uid] = $groupsByMember;
810
+                $groups = array_merge($groups, $groupsByMember);
811
+            }
812
+        }
813
+
814
+        if ($primaryGroup !== false) {
815
+            $groups[] = $primaryGroup;
816
+        }
817
+        if ($gidGroupName !== false) {
818
+            $groups[] = $gidGroupName;
819
+        }
820
+
821
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
822
+        $this->access->connection->writeToCache($cacheKey, $groups);
823
+
824
+        return $groups;
825
+    }
826
+
827
+    /**
828
+     * @throws ServerNotAvailableException
829
+     */
830
+    private function getGroupsByMember(string $dn, array &$seen = null): array {
831
+        if ($seen === null) {
832
+            $seen = [];
833
+        }
834
+        if (array_key_exists($dn, $seen)) {
835
+            // avoid loops
836
+            return [];
837
+        }
838
+        $allGroups = [];
839
+        $seen[$dn] = true;
840
+        $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
841
+
842
+        if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
843
+            //in this case the member entries are email addresses
844
+            $filter .= '@*';
845
+        }
846
+
847
+        $nesting = (int)$this->access->connection->ldapNestedGroups;
848
+        if ($nesting === 0) {
849
+            $filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
850
+        }
851
+
852
+        $groups = $this->access->fetchListOfGroups($filter,
853
+            [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
854
+        if (is_array($groups)) {
855
+            $fetcher = function ($dn) use (&$seen) {
856
+                if (is_array($dn) && isset($dn['dn'][0])) {
857
+                    $dn = $dn['dn'][0];
858
+                }
859
+                return $this->getGroupsByMember($dn, $seen);
860
+            };
861
+
862
+            if (empty($dn)) {
863
+                $dn = "";
864
+            }
865
+
866
+            $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups, $seen);
867
+        }
868
+        $visibleGroups = $this->filterValidGroups($allGroups);
869
+        return array_intersect_key($allGroups, $visibleGroups);
870
+    }
871
+
872
+    /**
873
+     * get a list of all users in a group
874
+     *
875
+     * @param string $gid
876
+     * @param string $search
877
+     * @param int $limit
878
+     * @param int $offset
879
+     * @return array with user ids
880
+     * @throws Exception
881
+     * @throws ServerNotAvailableException
882
+     */
883
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
884
+        if (!$this->enabled) {
885
+            return [];
886
+        }
887
+        if (!$this->groupExists($gid)) {
888
+            return [];
889
+        }
890
+        $search = $this->access->escapeFilterPart($search, true);
891
+        $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
892
+        // check for cache of the exact query
893
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
894
+        if (!is_null($groupUsers)) {
895
+            return $groupUsers;
896
+        }
897
+
898
+        if ($limit === -1) {
899
+            $limit = null;
900
+        }
901
+        // check for cache of the query without limit and offset
902
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
903
+        if (!is_null($groupUsers)) {
904
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
905
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
906
+            return $groupUsers;
907
+        }
908
+
909
+        $groupDN = $this->access->groupname2dn($gid);
910
+        if (!$groupDN) {
911
+            // group couldn't be found, return empty resultset
912
+            $this->access->connection->writeToCache($cacheKey, []);
913
+            return [];
914
+        }
915
+
916
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
917
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
918
+        $members = $this->_groupMembers($groupDN);
919
+        if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
920
+            //in case users could not be retrieved, return empty result set
921
+            $this->access->connection->writeToCache($cacheKey, []);
922
+            return [];
923
+        }
924
+
925
+        $groupUsers = [];
926
+        $attrs = $this->access->userManager->getAttributes(true);
927
+        foreach ($members as $member) {
928
+            switch ($this->ldapGroupMemberAssocAttr) {
929
+                /** @noinspection PhpMissingBreakStatementInspection */
930
+                case 'zimbramailforwardingaddress':
931
+                    //we get email addresses and need to convert them to uids
932
+                    $parts = explode('@', $member);
933
+                    $member = $parts[0];
934
+                    //no break needed because we just needed to remove the email part and now we have uids
935
+                case 'memberuid':
936
+                    //we got uids, need to get their DNs to 'translate' them to user names
937
+                    $filter = $this->access->combineFilterWithAnd([
938
+                        str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
939
+                        $this->access->combineFilterWithAnd([
940
+                            $this->access->getFilterPartForUserSearch($search),
941
+                            $this->access->connection->ldapUserFilter
942
+                        ])
943
+                    ]);
944
+                    $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
945
+                    if (empty($ldap_users)) {
946
+                        break;
947
+                    }
948
+                    $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
949
+                    break;
950
+                default:
951
+                    //we got DNs, check if we need to filter by search or we can give back all of them
952
+                    $uid = $this->access->dn2username($member);
953
+                    if (!$uid) {
954
+                        break;
955
+                    }
956
+
957
+                    $cacheKey = 'userExistsOnLDAP' . $uid;
958
+                    $userExists = $this->access->connection->getFromCache($cacheKey);
959
+                    if ($userExists === false) {
960
+                        break;
961
+                    }
962
+                    if ($userExists === null || $search !== '') {
963
+                        if (!$this->access->readAttribute($member,
964
+                            $this->access->connection->ldapUserDisplayName,
965
+                            $this->access->combineFilterWithAnd([
966
+                                $this->access->getFilterPartForUserSearch($search),
967
+                                $this->access->connection->ldapUserFilter
968
+                            ]))) {
969
+                            if ($search === '') {
970
+                                $this->access->connection->writeToCache($cacheKey, false);
971
+                            }
972
+                            break;
973
+                        }
974
+                        $this->access->connection->writeToCache($cacheKey, true);
975
+                    }
976
+                    $groupUsers[] = $uid;
977
+                    break;
978
+            }
979
+        }
980
+
981
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
982
+        natsort($groupUsers);
983
+        $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
984
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
985
+
986
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
987
+
988
+        return $groupUsers;
989
+    }
990
+
991
+    /**
992
+     * returns the number of users in a group, who match the search term
993
+     *
994
+     * @param string $gid the internal group name
995
+     * @param string $search optional, a search string
996
+     * @return int|bool
997
+     * @throws Exception
998
+     * @throws ServerNotAvailableException
999
+     */
1000
+    public function countUsersInGroup($gid, $search = '') {
1001
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
1002
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
1003
+        }
1004
+
1005
+        $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
1006
+        if (!$this->enabled || !$this->groupExists($gid)) {
1007
+            return false;
1008
+        }
1009
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
1010
+        if (!is_null($groupUsers)) {
1011
+            return $groupUsers;
1012
+        }
1013
+
1014
+        $groupDN = $this->access->groupname2dn($gid);
1015
+        if (!$groupDN) {
1016
+            // group couldn't be found, return empty result set
1017
+            $this->access->connection->writeToCache($cacheKey, false);
1018
+            return false;
1019
+        }
1020
+
1021
+        $members = $this->_groupMembers($groupDN);
1022
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
1023
+        if (!$members && $primaryUserCount === 0) {
1024
+            //in case users could not be retrieved, return empty result set
1025
+            $this->access->connection->writeToCache($cacheKey, false);
1026
+            return false;
1027
+        }
1028
+
1029
+        if ($search === '') {
1030
+            $groupUsers = count($members) + $primaryUserCount;
1031
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
1032
+            return $groupUsers;
1033
+        }
1034
+        $search = $this->access->escapeFilterPart($search, true);
1035
+        $isMemberUid =
1036
+            ($this->ldapGroupMemberAssocAttr === 'memberuid' ||
1037
+                $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
1038
+
1039
+        //we need to apply the search filter
1040
+        //alternatives that need to be checked:
1041
+        //a) get all users by search filter and array_intersect them
1042
+        //b) a, but only when less than 1k 10k ?k users like it is
1043
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
1044
+        //   and let it count.
1045
+        //For now this is not important, because the only use of this method
1046
+        //does not supply a search string
1047
+        $groupUsers = [];
1048
+        foreach ($members as $member) {
1049
+            if ($isMemberUid) {
1050
+                if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
1051
+                    //we get email addresses and need to convert them to uids
1052
+                    $parts = explode('@', $member);
1053
+                    $member = $parts[0];
1054
+                }
1055
+                //we got uids, need to get their DNs to 'translate' them to user names
1056
+                $filter = $this->access->combineFilterWithAnd([
1057
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
1058
+                    $this->access->getFilterPartForUserSearch($search)
1059
+                ]);
1060
+                $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
1061
+                if (count($ldap_users) < 1) {
1062
+                    continue;
1063
+                }
1064
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
1065
+            } else {
1066
+                //we need to apply the search filter now
1067
+                if (!$this->access->readAttribute($member,
1068
+                    $this->access->connection->ldapUserDisplayName,
1069
+                    $this->access->getFilterPartForUserSearch($search))) {
1070
+                    continue;
1071
+                }
1072
+                // dn2username will also check if the users belong to the allowed base
1073
+                if ($ncGroupId = $this->access->dn2username($member)) {
1074
+                    $groupUsers[] = $ncGroupId;
1075
+                }
1076
+            }
1077
+        }
1078
+
1079
+        //and get users that have the group as primary
1080
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1081
+
1082
+        return count($groupUsers) + $primaryUsers;
1083
+    }
1084
+
1085
+    /**
1086
+     * get a list of all groups using a paged search
1087
+     *
1088
+     * @param string $search
1089
+     * @param int $limit
1090
+     * @param int $offset
1091
+     * @return array with group names
1092
+     *
1093
+     * Returns a list with all groups
1094
+     * Uses a paged search if available to override a
1095
+     * server side search limit.
1096
+     * (active directory has a limit of 1000 by default)
1097
+     * @throws Exception
1098
+     */
1099
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1100
+        if (!$this->enabled) {
1101
+            return [];
1102
+        }
1103
+        $search = $this->access->escapeFilterPart($search, true);
1104
+        $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1105
+
1106
+        //Check cache before driving unnecessary searches
1107
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
1108
+        if (!is_null($ldap_groups)) {
1109
+            return $ldap_groups;
1110
+        }
1111
+
1112
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
1113
+        // error. With a limit of 0, we get 0 results. So we pass null.
1114
+        if ($limit <= 0) {
1115
+            $limit = null;
1116
+        }
1117
+        $filter = $this->access->combineFilterWithAnd([
1118
+            $this->access->connection->ldapGroupFilter,
1119
+            $this->access->getFilterPartForGroupSearch($search)
1120
+        ]);
1121
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
1122
+            [$this->access->connection->ldapGroupDisplayName, 'dn'],
1123
+            $limit,
1124
+            $offset);
1125
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1126
+
1127
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
1128
+        return $ldap_groups;
1129
+    }
1130
+
1131
+    /**
1132
+     * check if a group exists
1133
+     *
1134
+     * @param string $gid
1135
+     * @return bool
1136
+     * @throws ServerNotAvailableException
1137
+     */
1138
+    public function groupExists($gid) {
1139
+        $groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1140
+        if (!is_null($groupExists)) {
1141
+            return (bool)$groupExists;
1142
+        }
1143
+
1144
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1145
+        //only, requires more checking.
1146
+        $dn = $this->access->groupname2dn($gid);
1147
+        if (!$dn) {
1148
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1149
+            return false;
1150
+        }
1151
+
1152
+        if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1153
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1154
+            return false;
1155
+        }
1156
+
1157
+        //if group really still exists, we will be able to read its objectClass
1158
+        if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1159
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1160
+            return false;
1161
+        }
1162
+
1163
+        $this->access->connection->writeToCache('groupExists' . $gid, true);
1164
+        return true;
1165
+    }
1166
+
1167
+    /**
1168
+     * @throws ServerNotAvailableException
1169
+     * @throws Exception
1170
+     */
1171
+    protected function filterValidGroups(array $listOfGroups): array {
1172
+        $validGroupDNs = [];
1173
+        foreach ($listOfGroups as $key => $item) {
1174
+            $dn = is_string($item) ? $item : $item['dn'][0];
1175
+            $gid = $this->access->dn2groupname($dn);
1176
+            if (!$gid) {
1177
+                continue;
1178
+            }
1179
+            if ($this->groupExists($gid)) {
1180
+                $validGroupDNs[$key] = $item;
1181
+            }
1182
+        }
1183
+        return $validGroupDNs;
1184
+    }
1185
+
1186
+    /**
1187
+     * Check if backend implements actions
1188
+     *
1189
+     * @param int $actions bitwise-or'ed actions
1190
+     * @return boolean
1191
+     *
1192
+     * Returns the supported actions as int to be
1193
+     * compared with GroupInterface::CREATE_GROUP etc.
1194
+     */
1195
+    public function implementsActions($actions) {
1196
+        return (bool)((GroupInterface::COUNT_USERS |
1197
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1198
+    }
1199
+
1200
+    /**
1201
+     * Return access for LDAP interaction.
1202
+     *
1203
+     * @return Access instance of Access for LDAP interaction
1204
+     */
1205
+    public function getLDAPAccess($gid) {
1206
+        return $this->access;
1207
+    }
1208
+
1209
+    /**
1210
+     * create a group
1211
+     *
1212
+     * @param string $gid
1213
+     * @return bool
1214
+     * @throws Exception
1215
+     * @throws ServerNotAvailableException
1216
+     */
1217
+    public function createGroup($gid) {
1218
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1219
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1220
+                //updates group mapping
1221
+                $uuid = $this->access->getUUID($dn, false);
1222
+                if (is_string($uuid)) {
1223
+                    $this->access->mapAndAnnounceIfApplicable(
1224
+                        $this->access->getGroupMapper(),
1225
+                        $dn,
1226
+                        $gid,
1227
+                        $uuid,
1228
+                        false
1229
+                    );
1230
+                    $this->access->cacheGroupExists($gid);
1231
+                }
1232
+            }
1233
+            return $dn != null;
1234
+        }
1235
+        throw new Exception('Could not create group in LDAP backend.');
1236
+    }
1237
+
1238
+    /**
1239
+     * delete a group
1240
+     *
1241
+     * @param string $gid gid of the group to delete
1242
+     * @return bool
1243
+     * @throws Exception
1244
+     */
1245
+    public function deleteGroup($gid) {
1246
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1247
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1248
+                #delete group in nextcloud internal db
1249
+                $this->access->getGroupMapper()->unmap($gid);
1250
+                $this->access->connection->writeToCache("groupExists" . $gid, false);
1251
+            }
1252
+            return $ret;
1253
+        }
1254
+        throw new Exception('Could not delete group in LDAP backend.');
1255
+    }
1256
+
1257
+    /**
1258
+     * Add a user to a group
1259
+     *
1260
+     * @param string $uid Name of the user to add to group
1261
+     * @param string $gid Name of the group in which add the user
1262
+     * @return bool
1263
+     * @throws Exception
1264
+     */
1265
+    public function addToGroup($uid, $gid) {
1266
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1267
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1268
+                $this->access->connection->clearCache();
1269
+                unset($this->cachedGroupMembers[$gid]);
1270
+            }
1271
+            return $ret;
1272
+        }
1273
+        throw new Exception('Could not add user to group in LDAP backend.');
1274
+    }
1275
+
1276
+    /**
1277
+     * Removes a user from a group
1278
+     *
1279
+     * @param string $uid Name of the user to remove from group
1280
+     * @param string $gid Name of the group from which remove the user
1281
+     * @return bool
1282
+     * @throws Exception
1283
+     */
1284
+    public function removeFromGroup($uid, $gid) {
1285
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1286
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1287
+                $this->access->connection->clearCache();
1288
+                unset($this->cachedGroupMembers[$gid]);
1289
+            }
1290
+            return $ret;
1291
+        }
1292
+        throw new Exception('Could not remove user from group in LDAP backend.');
1293
+    }
1294
+
1295
+    /**
1296
+     * Gets group details
1297
+     *
1298
+     * @param string $gid Name of the group
1299
+     * @return array|false
1300
+     * @throws Exception
1301
+     */
1302
+    public function getGroupDetails($gid) {
1303
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1304
+            return $this->groupPluginManager->getGroupDetails($gid);
1305
+        }
1306
+        throw new Exception('Could not get group details in LDAP backend.');
1307
+    }
1308
+
1309
+    /**
1310
+     * Return LDAP connection resource from a cloned connection.
1311
+     * The cloned connection needs to be closed manually.
1312
+     * of the current access.
1313
+     *
1314
+     * @param string $gid
1315
+     * @return resource of the LDAP connection
1316
+     * @throws ServerNotAvailableException
1317
+     */
1318
+    public function getNewLDAPConnection($gid) {
1319
+        $connection = clone $this->access->getConnection();
1320
+        return $connection->getConnectionResource();
1321
+    }
1322
+
1323
+    /**
1324
+     * @throws ServerNotAvailableException
1325
+     */
1326
+    public function getDisplayName(string $gid): string {
1327
+        if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1328
+            return $this->groupPluginManager->getDisplayName($gid);
1329
+        }
1330
+
1331
+        $cacheKey = 'group_getDisplayName' . $gid;
1332
+        if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1333
+            return $displayName;
1334
+        }
1335
+
1336
+        $displayName = $this->access->readAttribute(
1337
+            $this->access->groupname2dn($gid),
1338
+            $this->access->connection->ldapGroupDisplayName);
1339
+
1340
+        if ($displayName && (count($displayName) > 0)) {
1341
+            $displayName = $displayName[0];
1342
+            $this->access->connection->writeToCache($cacheKey, $displayName);
1343
+            return $displayName;
1344
+        }
1345
+
1346
+        return '';
1347
+    }
1348 1348
 }
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;
@@ -254,7 +254,7 @@  discard block
 block discarded – undo
254 254
 			return [];
255 255
 		}
256 256
 		// used extensively in cron job, caching makes sense for nested groups
257
-		$cacheKey = '_groupMembers' . $dnGroup;
257
+		$cacheKey = '_groupMembers'.$dnGroup;
258 258
 		$groupMembers = $this->access->connection->getFromCache($cacheKey);
259 259
 		if ($groupMembers !== null) {
260 260
 			return $groupMembers;
@@ -269,14 +269,14 @@  discard block
 block discarded – undo
269 269
 			// compatibility hack with servers supporting :1.2.840.113556.1.4.1941:, and others)
270 270
 			$filter = $this->access->combineFilterWithAnd([
271 271
 				$this->access->connection->ldapUserFilter,
272
-				$this->access->connection->ldapUserDisplayName . '=*',
273
-				'memberof:1.2.840.113556.1.4.1941:=' . $dnGroup
272
+				$this->access->connection->ldapUserDisplayName.'=*',
273
+				'memberof:1.2.840.113556.1.4.1941:='.$dnGroup
274 274
 			]);
275 275
 			$memberRecords = $this->access->fetchListOfUsers(
276 276
 				$filter,
277 277
 				$this->access->userManager->getAttributes(true)
278 278
 			);
279
-			$result = array_reduce($memberRecords, function ($carry, $record) {
279
+			$result = array_reduce($memberRecords, function($carry, $record) {
280 280
 				$carry[] = $record['dn'][0];
281 281
 				return $carry;
282 282
 			}, []);
@@ -293,7 +293,7 @@  discard block
 block discarded – undo
293 293
 		$seen[$dnGroup] = 1;
294 294
 		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
295 295
 		if (is_array($members)) {
296
-			$fetcher = function ($memberDN) use (&$seen) {
296
+			$fetcher = function($memberDN) use (&$seen) {
297 297
 				return $this->_groupMembers($memberDN, $seen);
298 298
 			};
299 299
 			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members, $seen);
@@ -327,7 +327,7 @@  discard block
 block discarded – undo
327 327
 			return [];
328 328
 		}
329 329
 
330
-		$fetcher = function ($groupDN) {
330
+		$fetcher = function($groupDN) {
331 331
 			if (isset($this->cachedNestedGroups[$groupDN])) {
332 332
 				$nestedGroups = $this->cachedNestedGroups[$groupDN];
333 333
 			} else {
@@ -345,7 +345,7 @@  discard block
 block discarded – undo
345 345
 	}
346 346
 
347 347
 	private function walkNestedGroups(string $dn, Closure $fetcher, array $list, array &$seen = []): array {
348
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
348
+		$nesting = (int) $this->access->connection->ldapNestedGroups;
349 349
 		// depending on the input, we either have a list of DNs or a list of LDAP records
350 350
 		// also, the output expects either DNs or records. Testing the first element should suffice.
351 351
 		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
@@ -353,7 +353,7 @@  discard block
 block discarded – undo
353 353
 		if ($nesting !== 1) {
354 354
 			if ($recordMode) {
355 355
 				// the keys are numeric, but should hold the DN
356
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
356
+				return array_reduce($list, function($transformed, $record) use ($dn) {
357 357
 					if ($record['dn'][0] != $dn) {
358 358
 						$transformed[$record['dn'][0]] = $record;
359 359
 					}
@@ -388,7 +388,7 @@  discard block
 block discarded – undo
388 388
 	 * @throws ServerNotAvailableException
389 389
 	 */
390 390
 	public function gidNumber2Name(string $gid, string $dn) {
391
-		$cacheKey = 'gidNumberToName' . $gid;
391
+		$cacheKey = 'gidNumberToName'.$gid;
392 392
 		$groupName = $this->access->connection->getFromCache($cacheKey);
393 393
 		if (!is_null($groupName) && isset($groupName)) {
394 394
 			return $groupName;
@@ -398,7 +398,7 @@  discard block
 block discarded – undo
398 398
 		$filter = $this->access->combineFilterWithAnd([
399 399
 			$this->access->connection->ldapGroupFilter,
400 400
 			'objectClass=posixGroup',
401
-			$this->access->connection->ldapGidNumber . '=' . $gid
401
+			$this->access->connection->ldapGidNumber.'='.$gid
402 402
 		]);
403 403
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
404 404
 	}
@@ -478,7 +478,7 @@  discard block
 block discarded – undo
478 478
 		if ($search !== '') {
479 479
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
480 480
 		}
481
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
481
+		$filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID;
482 482
 
483 483
 		return $this->access->combineFilterWithAnd($filterParts);
484 484
 	}
@@ -548,7 +548,7 @@  discard block
 block discarded – undo
548 548
 		//we need to get the DN from LDAP
549 549
 		$filter = $this->access->combineFilterWithAnd([
550 550
 			$this->access->connection->ldapGroupFilter,
551
-			'objectsid=' . $domainObjectSid . '-' . $gid
551
+			'objectsid='.$domainObjectSid.'-'.$gid
552 552
 		]);
553 553
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
554 554
 	}
@@ -605,7 +605,7 @@  discard block
 block discarded – undo
605 605
 		if ($search !== '') {
606 606
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
607 607
 		}
608
-		$filterParts[] = 'primaryGroupID=' . $groupID;
608
+		$filterParts[] = 'primaryGroupID='.$groupID;
609 609
 
610 610
 		return $this->access->combineFilterWithAnd($filterParts);
611 611
 	}
@@ -647,7 +647,7 @@  discard block
 block discarded – undo
647 647
 		try {
648 648
 			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
649 649
 			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
650
-			return (int)$users;
650
+			return (int) $users;
651 651
 		} catch (ServerNotAvailableException $e) {
652 652
 			throw $e;
653 653
 		} catch (Exception $e) {
@@ -686,7 +686,7 @@  discard block
 block discarded – undo
686 686
 		if (!$this->enabled) {
687 687
 			return [];
688 688
 		}
689
-		$cacheKey = 'getUserGroups' . $uid;
689
+		$cacheKey = 'getUserGroups'.$uid;
690 690
 		$userGroups = $this->access->connection->getFromCache($cacheKey);
691 691
 		if (!is_null($userGroups)) {
692 692
 			return $userGroups;
@@ -744,8 +744,8 @@  discard block
 block discarded – undo
744 744
 		// if possible, read out membership via memberOf. It's far faster than
745 745
 		// performing a search, which still is a fallback later.
746 746
 		// memberof doesn't support memberuid, so skip it here.
747
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
748
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
747
+		if ((int) $this->access->connection->hasMemberOfFilterSupport === 1
748
+			&& (int) $this->access->connection->useMemberOfToDetectMembership === 1
749 749
 			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
750 750
 			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
751 751
 			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
@@ -837,14 +837,14 @@  discard block
 block discarded – undo
837 837
 		}
838 838
 		$allGroups = [];
839 839
 		$seen[$dn] = true;
840
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
840
+		$filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
841 841
 
842 842
 		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
843 843
 			//in this case the member entries are email addresses
844 844
 			$filter .= '@*';
845 845
 		}
846 846
 
847
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
847
+		$nesting = (int) $this->access->connection->ldapNestedGroups;
848 848
 		if ($nesting === 0) {
849 849
 			$filter = $this->access->combineFilterWithAnd([$filter, $this->access->connection->ldapGroupFilter]);
850 850
 		}
@@ -852,7 +852,7 @@  discard block
 block discarded – undo
852 852
 		$groups = $this->access->fetchListOfGroups($filter,
853 853
 			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
854 854
 		if (is_array($groups)) {
855
-			$fetcher = function ($dn) use (&$seen) {
855
+			$fetcher = function($dn) use (&$seen) {
856 856
 				if (is_array($dn) && isset($dn['dn'][0])) {
857 857
 					$dn = $dn['dn'][0];
858 858
 				}
@@ -888,7 +888,7 @@  discard block
 block discarded – undo
888 888
 			return [];
889 889
 		}
890 890
 		$search = $this->access->escapeFilterPart($search, true);
891
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
891
+		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
892 892
 		// check for cache of the exact query
893 893
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
894 894
 		if (!is_null($groupUsers)) {
@@ -899,7 +899,7 @@  discard block
 block discarded – undo
899 899
 			$limit = null;
900 900
 		}
901 901
 		// check for cache of the query without limit and offset
902
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
902
+		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
903 903
 		if (!is_null($groupUsers)) {
904 904
 			$groupUsers = array_slice($groupUsers, $offset, $limit);
905 905
 			$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -954,7 +954,7 @@  discard block
 block discarded – undo
954 954
 						break;
955 955
 					}
956 956
 
957
-					$cacheKey = 'userExistsOnLDAP' . $uid;
957
+					$cacheKey = 'userExistsOnLDAP'.$uid;
958 958
 					$userExists = $this->access->connection->getFromCache($cacheKey);
959 959
 					if ($userExists === false) {
960 960
 						break;
@@ -980,7 +980,7 @@  discard block
 block discarded – undo
980 980
 
981 981
 		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
982 982
 		natsort($groupUsers);
983
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
983
+		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
984 984
 		$groupUsers = array_slice($groupUsers, $offset, $limit);
985 985
 
986 986
 		$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -1002,7 +1002,7 @@  discard block
 block discarded – undo
1002 1002
 			return $this->groupPluginManager->countUsersInGroup($gid, $search);
1003 1003
 		}
1004 1004
 
1005
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
1005
+		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
1006 1006
 		if (!$this->enabled || !$this->groupExists($gid)) {
1007 1007
 			return false;
1008 1008
 		}
@@ -1101,7 +1101,7 @@  discard block
 block discarded – undo
1101 1101
 			return [];
1102 1102
 		}
1103 1103
 		$search = $this->access->escapeFilterPart($search, true);
1104
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1104
+		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
1105 1105
 
1106 1106
 		//Check cache before driving unnecessary searches
1107 1107
 		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
@@ -1136,31 +1136,31 @@  discard block
 block discarded – undo
1136 1136
 	 * @throws ServerNotAvailableException
1137 1137
 	 */
1138 1138
 	public function groupExists($gid) {
1139
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1139
+		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1140 1140
 		if (!is_null($groupExists)) {
1141
-			return (bool)$groupExists;
1141
+			return (bool) $groupExists;
1142 1142
 		}
1143 1143
 
1144 1144
 		//getting dn, if false the group does not exist. If dn, it may be mapped
1145 1145
 		//only, requires more checking.
1146 1146
 		$dn = $this->access->groupname2dn($gid);
1147 1147
 		if (!$dn) {
1148
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1148
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1149 1149
 			return false;
1150 1150
 		}
1151 1151
 
1152 1152
 		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1153
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1153
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1154 1154
 			return false;
1155 1155
 		}
1156 1156
 
1157 1157
 		//if group really still exists, we will be able to read its objectClass
1158 1158
 		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1159
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1159
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1160 1160
 			return false;
1161 1161
 		}
1162 1162
 
1163
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1163
+		$this->access->connection->writeToCache('groupExists'.$gid, true);
1164 1164
 		return true;
1165 1165
 	}
1166 1166
 
@@ -1193,7 +1193,7 @@  discard block
 block discarded – undo
1193 1193
 	 * compared with GroupInterface::CREATE_GROUP etc.
1194 1194
 	 */
1195 1195
 	public function implementsActions($actions) {
1196
-		return (bool)((GroupInterface::COUNT_USERS |
1196
+		return (bool) ((GroupInterface::COUNT_USERS |
1197 1197
 				$this->groupPluginManager->getImplementedActions()) & $actions);
1198 1198
 	}
1199 1199
 
@@ -1247,7 +1247,7 @@  discard block
 block discarded – undo
1247 1247
 			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1248 1248
 				#delete group in nextcloud internal db
1249 1249
 				$this->access->getGroupMapper()->unmap($gid);
1250
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1250
+				$this->access->connection->writeToCache("groupExists".$gid, false);
1251 1251
 			}
1252 1252
 			return $ret;
1253 1253
 		}
@@ -1328,7 +1328,7 @@  discard block
 block discarded – undo
1328 1328
 			return $this->groupPluginManager->getDisplayName($gid);
1329 1329
 		}
1330 1330
 
1331
-		$cacheKey = 'group_getDisplayName' . $gid;
1331
+		$cacheKey = 'group_getDisplayName'.$gid;
1332 1332
 		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1333 1333
 			return $displayName;
1334 1334
 		}
Please login to merge, or discard this patch.