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