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