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