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