Passed
Push — master ( 326a04...579c70 )
by Blizzz
12:55 queued 11s
created
apps/user_ldap/lib/Group_LDAP.php 2 patches
Indentation   +1225 added lines, -1225 removed lines patch added patch discarded remove patch
@@ -54,1229 +54,1229 @@
 block discarded – undo
54 54
 use OCP\ILogger;
55 55
 
56 56
 class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
57
-	protected $enabled = false;
58
-
59
-	/** @var string[] $cachedGroupMembers array of users with gid as key */
60
-	protected $cachedGroupMembers;
61
-	/** @var string[] $cachedGroupsByMember array of groups with uid as key */
62
-	protected $cachedGroupsByMember;
63
-	/** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
64
-	protected $cachedNestedGroups;
65
-	/** @var GroupPluginManager */
66
-	protected $groupPluginManager;
67
-	/** @var ILogger */
68
-	protected $logger;
69
-
70
-	/**
71
-	 * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
72
-	 */
73
-	protected $ldapGroupMemberAssocAttr;
74
-
75
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
76
-		parent::__construct($access);
77
-		$filter = $this->access->connection->ldapGroupFilter;
78
-		$gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
79
-		if (!empty($filter) && !empty($gAssoc)) {
80
-			$this->enabled = true;
81
-		}
82
-
83
-		$this->cachedGroupMembers = new CappedMemoryCache();
84
-		$this->cachedGroupsByMember = new CappedMemoryCache();
85
-		$this->cachedNestedGroups = new CappedMemoryCache();
86
-		$this->groupPluginManager = $groupPluginManager;
87
-		$this->logger = OC::$server->getLogger();
88
-		$this->ldapGroupMemberAssocAttr = strtolower($gAssoc);
89
-	}
90
-
91
-	/**
92
-	 * is user in group?
93
-	 *
94
-	 * @param string $uid uid of the user
95
-	 * @param string $gid gid of the group
96
-	 * @return bool
97
-	 * @throws Exception
98
-	 * @throws ServerNotAvailableException
99
-	 */
100
-	public function inGroup($uid, $gid) {
101
-		if (!$this->enabled) {
102
-			return false;
103
-		}
104
-		$cacheKey = 'inGroup' . $uid . ':' . $gid;
105
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
106
-		if (!is_null($inGroup)) {
107
-			return (bool)$inGroup;
108
-		}
109
-
110
-		$userDN = $this->access->username2dn($uid);
111
-
112
-		if (isset($this->cachedGroupMembers[$gid])) {
113
-			return in_array($userDN, $this->cachedGroupMembers[$gid]);
114
-		}
115
-
116
-		$cacheKeyMembers = 'inGroup-members:' . $gid;
117
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
118
-		if (!is_null($members)) {
119
-			$this->cachedGroupMembers[$gid] = $members;
120
-			$isInGroup = in_array($userDN, $members, true);
121
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
122
-			return $isInGroup;
123
-		}
124
-
125
-		$groupDN = $this->access->groupname2dn($gid);
126
-		// just in case
127
-		if (!$groupDN || !$userDN) {
128
-			$this->access->connection->writeToCache($cacheKey, false);
129
-			return false;
130
-		}
131
-
132
-		//check primary group first
133
-		if ($gid === $this->getUserPrimaryGroup($userDN)) {
134
-			$this->access->connection->writeToCache($cacheKey, true);
135
-			return true;
136
-		}
137
-
138
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
139
-		$members = $this->_groupMembers($groupDN);
140
-		if (!is_array($members) || count($members) === 0) {
141
-			$this->access->connection->writeToCache($cacheKey, false);
142
-			return false;
143
-		}
144
-
145
-		//extra work if we don't get back user DNs
146
-		switch ($this->ldapGroupMemberAssocAttr) {
147
-			case 'memberuid':
148
-			case 'zimbramailforwardingaddress':
149
-				$requestAttributes = $this->access->userManager->getAttributes(true);
150
-				$dns = [];
151
-				$filterParts = [];
152
-				$bytes = 0;
153
-				foreach ($members as $mid) {
154
-					if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
155
-						$parts = explode('@', $mid); //making sure we get only the uid
156
-						$mid = $parts[0];
157
-					}
158
-					$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
159
-					$filterParts[] = $filter;
160
-					$bytes += strlen($filter);
161
-					if ($bytes >= 9000000) {
162
-						// AD has a default input buffer of 10 MB, we do not want
163
-						// to take even the chance to exceed it
164
-						$filter = $this->access->combineFilterWithOr($filterParts);
165
-						$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
166
-						$bytes = 0;
167
-						$filterParts = [];
168
-						$dns = array_merge($dns, $users);
169
-					}
170
-				}
171
-				if (count($filterParts) > 0) {
172
-					$filter = $this->access->combineFilterWithOr($filterParts);
173
-					$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
174
-					$dns = array_merge($dns, $users);
175
-				}
176
-				$members = $dns;
177
-				break;
178
-		}
179
-
180
-		$isInGroup = in_array($userDN, $members);
181
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
182
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
183
-		$this->cachedGroupMembers[$gid] = $members;
184
-
185
-		return $isInGroup;
186
-	}
187
-
188
-	/**
189
-	 * For a group that has user membership defined by an LDAP search url
190
-	 * attribute returns the users that match the search url otherwise returns
191
-	 * an empty array.
192
-	 *
193
-	 * @throws ServerNotAvailableException
194
-	 */
195
-	public function getDynamicGroupMembers(string $dnGroup): array {
196
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
197
-
198
-		if (empty($dynamicGroupMemberURL)) {
199
-			return [];
200
-		}
201
-
202
-		$dynamicMembers = [];
203
-		$memberURLs = $this->access->readAttribute(
204
-			$dnGroup,
205
-			$dynamicGroupMemberURL,
206
-			$this->access->connection->ldapGroupFilter
207
-		);
208
-		if ($memberURLs !== false) {
209
-			// this group has the 'memberURL' attribute so this is a dynamic group
210
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
211
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
212
-			$pos = strpos($memberURLs[0], '(');
213
-			if ($pos !== false) {
214
-				$memberUrlFilter = substr($memberURLs[0], $pos);
215
-				$foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
216
-				$dynamicMembers = [];
217
-				foreach ($foundMembers as $value) {
218
-					$dynamicMembers[$value['dn'][0]] = 1;
219
-				}
220
-			} else {
221
-				$this->logger->debug('No search filter found on member url of group {dn}',
222
-					[
223
-						'app' => 'user_ldap',
224
-						'dn' => $dnGroup,
225
-					]
226
-				);
227
-			}
228
-		}
229
-		return $dynamicMembers;
230
-	}
231
-
232
-	/**
233
-	 * @throws ServerNotAvailableException
234
-	 */
235
-	private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
236
-		if ($seen === null) {
237
-			$seen = [];
238
-		}
239
-		$allMembers = [];
240
-		if (array_key_exists($dnGroup, $seen)) {
241
-			return [];
242
-		}
243
-		// used extensively in cron job, caching makes sense for nested groups
244
-		$cacheKey = '_groupMembers' . $dnGroup;
245
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
246
-		if ($groupMembers !== null) {
247
-			return $groupMembers;
248
-		}
249
-		$seen[$dnGroup] = 1;
250
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
251
-		if (is_array($members)) {
252
-			$fetcher = function ($memberDN, &$seen) {
253
-				return $this->_groupMembers($memberDN, $seen);
254
-			};
255
-			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
256
-		}
257
-
258
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
259
-
260
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
261
-		return $allMembers;
262
-	}
263
-
264
-	/**
265
-	 * @throws ServerNotAvailableException
266
-	 */
267
-	private function _getGroupDNsFromMemberOf(string $dn): array {
268
-		$groups = $this->access->readAttribute($dn, 'memberOf');
269
-		if (!is_array($groups)) {
270
-			return [];
271
-		}
272
-
273
-		$fetcher = function ($groupDN) {
274
-			if (isset($this->cachedNestedGroups[$groupDN])) {
275
-				$nestedGroups = $this->cachedNestedGroups[$groupDN];
276
-			} else {
277
-				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
278
-				if (!is_array($nestedGroups)) {
279
-					$nestedGroups = [];
280
-				}
281
-				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
282
-			}
283
-			return $nestedGroups;
284
-		};
285
-
286
-		$groups = $this->walkNestedGroups($dn, $fetcher, $groups);
287
-		return $this->filterValidGroups($groups);
288
-	}
289
-
290
-	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
291
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
292
-		// depending on the input, we either have a list of DNs or a list of LDAP records
293
-		// also, the output expects either DNs or records. Testing the first element should suffice.
294
-		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
295
-
296
-		if ($nesting !== 1) {
297
-			if ($recordMode) {
298
-				// the keys are numeric, but should hold the DN
299
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
300
-					if ($record['dn'][0] != $dn) {
301
-						$transformed[$record['dn'][0]] = $record;
302
-					}
303
-					return $transformed;
304
-				}, []);
305
-			}
306
-			return $list;
307
-		}
308
-
309
-		$seen = [];
310
-		while ($record = array_pop($list)) {
311
-			$recordDN = $recordMode ? $record['dn'][0] : $record;
312
-			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
313
-				// Prevent loops
314
-				continue;
315
-			}
316
-			$fetched = $fetcher($record, $seen);
317
-			$list = array_merge($list, $fetched);
318
-			$seen[$recordDN] = $record;
319
-		}
320
-
321
-		return $recordMode ? $seen : array_keys($seen);
322
-	}
323
-
324
-	/**
325
-	 * translates a gidNumber into an ownCloud internal name
326
-	 *
327
-	 * @return string|bool
328
-	 * @throws Exception
329
-	 * @throws ServerNotAvailableException
330
-	 */
331
-	public function gidNumber2Name(string $gid, string $dn) {
332
-		$cacheKey = 'gidNumberToName' . $gid;
333
-		$groupName = $this->access->connection->getFromCache($cacheKey);
334
-		if (!is_null($groupName) && isset($groupName)) {
335
-			return $groupName;
336
-		}
337
-
338
-		//we need to get the DN from LDAP
339
-		$filter = $this->access->combineFilterWithAnd([
340
-			$this->access->connection->ldapGroupFilter,
341
-			'objectClass=posixGroup',
342
-			$this->access->connection->ldapGidNumber . '=' . $gid
343
-		]);
344
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
345
-	}
346
-
347
-	/**
348
-	 * @throws ServerNotAvailableException
349
-	 * @throws Exception
350
-	 */
351
-	private function getNameOfGroup(string $filter, string $cacheKey) {
352
-		$result = $this->access->searchGroups($filter, ['dn'], 1);
353
-		if (empty($result)) {
354
-			return null;
355
-		}
356
-		$dn = $result[0]['dn'][0];
357
-
358
-		//and now the group name
359
-		//NOTE once we have separate Nextcloud group IDs and group names we can
360
-		//directly read the display name attribute instead of the DN
361
-		$name = $this->access->dn2groupname($dn);
362
-
363
-		$this->access->connection->writeToCache($cacheKey, $name);
364
-
365
-		return $name;
366
-	}
367
-
368
-	/**
369
-	 * returns the entry's gidNumber
370
-	 *
371
-	 * @return string|bool
372
-	 * @throws ServerNotAvailableException
373
-	 */
374
-	private function getEntryGidNumber(string $dn, string $attribute) {
375
-		$value = $this->access->readAttribute($dn, $attribute);
376
-		if (is_array($value) && !empty($value)) {
377
-			return $value[0];
378
-		}
379
-		return false;
380
-	}
381
-
382
-	/**
383
-	 * @return string|bool
384
-	 * @throws ServerNotAvailableException
385
-	 */
386
-	public function getGroupGidNumber(string $dn) {
387
-		return $this->getEntryGidNumber($dn, 'gidNumber');
388
-	}
389
-
390
-	/**
391
-	 * returns the user's gidNumber
392
-	 *
393
-	 * @return string|bool
394
-	 * @throws ServerNotAvailableException
395
-	 */
396
-	public function getUserGidNumber(string $dn) {
397
-		$gidNumber = false;
398
-		if ($this->access->connection->hasGidNumber) {
399
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
400
-			if ($gidNumber === false) {
401
-				$this->access->connection->hasGidNumber = false;
402
-			}
403
-		}
404
-		return $gidNumber;
405
-	}
406
-
407
-	/**
408
-	 * @throws ServerNotAvailableException
409
-	 * @throws Exception
410
-	 */
411
-	private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
412
-		$groupID = $this->getGroupGidNumber($groupDN);
413
-		if ($groupID === false) {
414
-			throw new Exception('Not a valid group');
415
-		}
416
-
417
-		$filterParts = [];
418
-		$filterParts[] = $this->access->getFilterForUserCount();
419
-		if ($search !== '') {
420
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
421
-		}
422
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
423
-
424
-		return $this->access->combineFilterWithAnd($filterParts);
425
-	}
426
-
427
-	/**
428
-	 * returns a list of users that have the given group as gid number
429
-	 *
430
-	 * @throws ServerNotAvailableException
431
-	 */
432
-	public function getUsersInGidNumber(
433
-		string $groupDN,
434
-		string $search = '',
435
-		?int $limit = -1,
436
-		?int $offset = 0
437
-	): array {
438
-		try {
439
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
440
-			$users = $this->access->fetchListOfUsers(
441
-				$filter,
442
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
443
-				$limit,
444
-				$offset
445
-			);
446
-			return $this->access->nextcloudUserNames($users);
447
-		} catch (ServerNotAvailableException $e) {
448
-			throw $e;
449
-		} catch (Exception $e) {
450
-			return [];
451
-		}
452
-	}
453
-
454
-	/**
455
-	 * @throws ServerNotAvailableException
456
-	 * @return bool
457
-	 */
458
-	public function getUserGroupByGid(string $dn) {
459
-		$groupID = $this->getUserGidNumber($dn);
460
-		if ($groupID !== false) {
461
-			$groupName = $this->gidNumber2Name($groupID, $dn);
462
-			if ($groupName !== false) {
463
-				return $groupName;
464
-			}
465
-		}
466
-
467
-		return false;
468
-	}
469
-
470
-	/**
471
-	 * translates a primary group ID into an Nextcloud internal name
472
-	 *
473
-	 * @return string|bool
474
-	 * @throws Exception
475
-	 * @throws ServerNotAvailableException
476
-	 */
477
-	public function primaryGroupID2Name(string $gid, string $dn) {
478
-		$cacheKey = 'primaryGroupIDtoName';
479
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
480
-		if (!is_null($groupNames) && isset($groupNames[$gid])) {
481
-			return $groupNames[$gid];
482
-		}
483
-
484
-		$domainObjectSid = $this->access->getSID($dn);
485
-		if ($domainObjectSid === false) {
486
-			return false;
487
-		}
488
-
489
-		//we need to get the DN from LDAP
490
-		$filter = $this->access->combineFilterWithAnd([
491
-			$this->access->connection->ldapGroupFilter,
492
-			'objectsid=' . $domainObjectSid . '-' . $gid
493
-		]);
494
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
495
-	}
496
-
497
-	/**
498
-	 * returns the entry's primary group ID
499
-	 *
500
-	 * @return string|bool
501
-	 * @throws ServerNotAvailableException
502
-	 */
503
-	private function getEntryGroupID(string $dn, string $attribute) {
504
-		$value = $this->access->readAttribute($dn, $attribute);
505
-		if (is_array($value) && !empty($value)) {
506
-			return $value[0];
507
-		}
508
-		return false;
509
-	}
510
-
511
-	/**
512
-	 * @return string|bool
513
-	 * @throws ServerNotAvailableException
514
-	 */
515
-	public function getGroupPrimaryGroupID(string $dn) {
516
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
517
-	}
518
-
519
-	/**
520
-	 * @return string|bool
521
-	 * @throws ServerNotAvailableException
522
-	 */
523
-	public function getUserPrimaryGroupIDs(string $dn) {
524
-		$primaryGroupID = false;
525
-		if ($this->access->connection->hasPrimaryGroups) {
526
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
527
-			if ($primaryGroupID === false) {
528
-				$this->access->connection->hasPrimaryGroups = false;
529
-			}
530
-		}
531
-		return $primaryGroupID;
532
-	}
533
-
534
-	/**
535
-	 * @throws Exception
536
-	 * @throws ServerNotAvailableException
537
-	 */
538
-	private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
539
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
540
-		if ($groupID === false) {
541
-			throw new Exception('Not a valid group');
542
-		}
543
-
544
-		$filterParts = [];
545
-		$filterParts[] = $this->access->getFilterForUserCount();
546
-		if ($search !== '') {
547
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
548
-		}
549
-		$filterParts[] = 'primaryGroupID=' . $groupID;
550
-
551
-		return $this->access->combineFilterWithAnd($filterParts);
552
-	}
553
-
554
-	/**
555
-	 * @throws ServerNotAvailableException
556
-	 */
557
-	public function getUsersInPrimaryGroup(
558
-		string $groupDN,
559
-		string $search = '',
560
-		?int $limit = -1,
561
-		?int $offset = 0
562
-	): array {
563
-		try {
564
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
565
-			$users = $this->access->fetchListOfUsers(
566
-				$filter,
567
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
568
-				$limit,
569
-				$offset
570
-			);
571
-			return $this->access->nextcloudUserNames($users);
572
-		} catch (ServerNotAvailableException $e) {
573
-			throw $e;
574
-		} catch (Exception $e) {
575
-			return [];
576
-		}
577
-	}
578
-
579
-	/**
580
-	 * @throws ServerNotAvailableException
581
-	 */
582
-	public function countUsersInPrimaryGroup(
583
-		string $groupDN,
584
-		string $search = '',
585
-		int $limit = -1,
586
-		int $offset = 0
587
-	): int {
588
-		try {
589
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
590
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
591
-			return (int)$users;
592
-		} catch (ServerNotAvailableException $e) {
593
-			throw $e;
594
-		} catch (Exception $e) {
595
-			return 0;
596
-		}
597
-	}
598
-
599
-	/**
600
-	 * @return string|bool
601
-	 * @throws ServerNotAvailableException
602
-	 */
603
-	public function getUserPrimaryGroup(string $dn) {
604
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
605
-		if ($groupID !== false) {
606
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
607
-			if ($groupName !== false) {
608
-				return $groupName;
609
-			}
610
-		}
611
-
612
-		return false;
613
-	}
614
-
615
-	/**
616
-	 * This function fetches all groups a user belongs to. It does not check
617
-	 * if the user exists at all.
618
-	 *
619
-	 * This function includes groups based on dynamic group membership.
620
-	 *
621
-	 * @param string $uid Name of the user
622
-	 * @return array with group names
623
-	 * @throws Exception
624
-	 * @throws ServerNotAvailableException
625
-	 */
626
-	public function getUserGroups($uid) {
627
-		if (!$this->enabled) {
628
-			return [];
629
-		}
630
-		$cacheKey = 'getUserGroups' . $uid;
631
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
632
-		if (!is_null($userGroups)) {
633
-			return $userGroups;
634
-		}
635
-		$userDN = $this->access->username2dn($uid);
636
-		if (!$userDN) {
637
-			$this->access->connection->writeToCache($cacheKey, []);
638
-			return [];
639
-		}
640
-
641
-		$groups = [];
642
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
643
-		$gidGroupName = $this->getUserGroupByGid($userDN);
644
-
645
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
646
-
647
-		if (!empty($dynamicGroupMemberURL)) {
648
-			// look through dynamic groups to add them to the result array if needed
649
-			$groupsToMatch = $this->access->fetchListOfGroups(
650
-				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
651
-			foreach ($groupsToMatch as $dynamicGroup) {
652
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
653
-					continue;
654
-				}
655
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
656
-				if ($pos !== false) {
657
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
658
-					// apply filter via ldap search to see if this user is in this
659
-					// dynamic group
660
-					$userMatch = $this->access->readAttribute(
661
-						$userDN,
662
-						$this->access->connection->ldapUserDisplayName,
663
-						$memberUrlFilter
664
-					);
665
-					if ($userMatch !== false) {
666
-						// match found so this user is in this group
667
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
668
-						if (is_string($groupName)) {
669
-							// be sure to never return false if the dn could not be
670
-							// resolved to a name, for whatever reason.
671
-							$groups[] = $groupName;
672
-						}
673
-					}
674
-				} else {
675
-					$this->logger->debug('No search filter found on member url of group {dn}',
676
-						[
677
-							'app' => 'user_ldap',
678
-							'dn' => $dynamicGroup,
679
-						]
680
-					);
681
-				}
682
-			}
683
-		}
684
-
685
-		// if possible, read out membership via memberOf. It's far faster than
686
-		// performing a search, which still is a fallback later.
687
-		// memberof doesn't support memberuid, so skip it here.
688
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
689
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
690
-			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
691
-			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
692
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
693
-			if (is_array($groupDNs)) {
694
-				foreach ($groupDNs as $dn) {
695
-					$groupName = $this->access->dn2groupname($dn);
696
-					if (is_string($groupName)) {
697
-						// be sure to never return false if the dn could not be
698
-						// resolved to a name, for whatever reason.
699
-						$groups[] = $groupName;
700
-					}
701
-				}
702
-			}
703
-
704
-			if ($primaryGroup !== false) {
705
-				$groups[] = $primaryGroup;
706
-			}
707
-			if ($gidGroupName !== false) {
708
-				$groups[] = $gidGroupName;
709
-			}
710
-			$this->access->connection->writeToCache($cacheKey, $groups);
711
-			return $groups;
712
-		}
713
-
714
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
715
-		switch ($this->ldapGroupMemberAssocAttr) {
716
-			case 'uniquemember':
717
-			case 'member':
718
-				$uid = $userDN;
719
-				break;
720
-
721
-			case 'memberuid':
722
-			case 'zimbramailforwardingaddress':
723
-				$result = $this->access->readAttribute($userDN, 'uid');
724
-				if ($result === false) {
725
-					$this->logger->debug('No uid attribute found for DN {dn} on {host}',
726
-						[
727
-							'app' => 'user_ldap',
728
-							'dn' => $userDN,
729
-							'host' => $this->access->connection->ldapHost,
730
-						]
731
-					);
732
-					$uid = false;
733
-				} else {
734
-					$uid = $result[0];
735
-				}
736
-				break;
737
-
738
-			default:
739
-				// just in case
740
-				$uid = $userDN;
741
-				break;
742
-		}
743
-
744
-		if ($uid !== false) {
745
-			if (isset($this->cachedGroupsByMember[$uid])) {
746
-				$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
747
-			} else {
748
-				$groupsByMember = array_values($this->getGroupsByMember($uid));
749
-				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
750
-				$this->cachedGroupsByMember[$uid] = $groupsByMember;
751
-				$groups = array_merge($groups, $groupsByMember);
752
-			}
753
-		}
754
-
755
-		if ($primaryGroup !== false) {
756
-			$groups[] = $primaryGroup;
757
-		}
758
-		if ($gidGroupName !== false) {
759
-			$groups[] = $gidGroupName;
760
-		}
761
-
762
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
763
-		$this->access->connection->writeToCache($cacheKey, $groups);
764
-
765
-		return $groups;
766
-	}
767
-
768
-	/**
769
-	 * @throws ServerNotAvailableException
770
-	 */
771
-	private function getGroupsByMember(string $dn, array &$seen = null): array {
772
-		if ($seen === null) {
773
-			$seen = [];
774
-		}
775
-		if (array_key_exists($dn, $seen)) {
776
-			// avoid loops
777
-			return [];
778
-		}
779
-		$allGroups = [];
780
-		$seen[$dn] = true;
781
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
782
-
783
-		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
784
-			//in this case the member entries are email addresses
785
-			$filter .= '@*';
786
-		}
787
-
788
-		$groups = $this->access->fetchListOfGroups($filter,
789
-			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
790
-		if (is_array($groups)) {
791
-			$fetcher = function ($dn, &$seen) {
792
-				if (is_array($dn) && isset($dn['dn'][0])) {
793
-					$dn = $dn['dn'][0];
794
-				}
795
-				return $this->getGroupsByMember($dn, $seen);
796
-			};
797
-
798
-			if (empty($dn)) {
799
-				$dn = "";
800
-			}
801
-
802
-			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
803
-		}
804
-		$visibleGroups = $this->filterValidGroups($allGroups);
805
-		return array_intersect_key($allGroups, $visibleGroups);
806
-	}
807
-
808
-	/**
809
-	 * get a list of all users in a group
810
-	 *
811
-	 * @param string $gid
812
-	 * @param string $search
813
-	 * @param int $limit
814
-	 * @param int $offset
815
-	 * @return array with user ids
816
-	 * @throws Exception
817
-	 * @throws ServerNotAvailableException
818
-	 */
819
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
820
-		if (!$this->enabled) {
821
-			return [];
822
-		}
823
-		if (!$this->groupExists($gid)) {
824
-			return [];
825
-		}
826
-		$search = $this->access->escapeFilterPart($search, true);
827
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
828
-		// check for cache of the exact query
829
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
830
-		if (!is_null($groupUsers)) {
831
-			return $groupUsers;
832
-		}
833
-
834
-		if ($limit === -1) {
835
-			$limit = null;
836
-		}
837
-		// check for cache of the query without limit and offset
838
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
839
-		if (!is_null($groupUsers)) {
840
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
841
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
842
-			return $groupUsers;
843
-		}
844
-
845
-		$groupDN = $this->access->groupname2dn($gid);
846
-		if (!$groupDN) {
847
-			// group couldn't be found, return empty resultset
848
-			$this->access->connection->writeToCache($cacheKey, []);
849
-			return [];
850
-		}
851
-
852
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
853
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
854
-		$members = $this->_groupMembers($groupDN);
855
-		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
856
-			//in case users could not be retrieved, return empty result set
857
-			$this->access->connection->writeToCache($cacheKey, []);
858
-			return [];
859
-		}
860
-
861
-		$groupUsers = [];
862
-		$attrs = $this->access->userManager->getAttributes(true);
863
-		foreach ($members as $member) {
864
-			switch ($this->ldapGroupMemberAssocAttr) {
865
-				case 'zimbramailforwardingaddress':
866
-					//we get email addresses and need to convert them to uids
867
-					$parts = explode('@', $member);
868
-					$member = $parts[0];
869
-					//no break needed because we just needed to remove the email part and now we have uids
870
-				case 'memberuid':
871
-					//we got uids, need to get their DNs to 'translate' them to user names
872
-					$filter = $this->access->combineFilterWithAnd([
873
-						str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
874
-						$this->access->combineFilterWithAnd([
875
-							$this->access->getFilterPartForUserSearch($search),
876
-							$this->access->connection->ldapUserFilter
877
-						])
878
-					]);
879
-					$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
880
-					if (count($ldap_users) < 1) {
881
-						continue;
882
-					}
883
-					$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
884
-					break;
885
-				default:
886
-					//we got DNs, check if we need to filter by search or we can give back all of them
887
-					$uid = $this->access->dn2username($member);
888
-					if (!$uid) {
889
-						continue;
890
-					}
891
-
892
-					$cacheKey = 'userExistsOnLDAP' . $uid;
893
-					$userExists = $this->access->connection->getFromCache($cacheKey);
894
-					if ($userExists === false) {
895
-						continue;
896
-					}
897
-					if ($userExists === null || $search !== '') {
898
-						if (!$this->access->readAttribute($member,
899
-							$this->access->connection->ldapUserDisplayName,
900
-							$this->access->combineFilterWithAnd([
901
-								$this->access->getFilterPartForUserSearch($search),
902
-								$this->access->connection->ldapUserFilter
903
-							]))) {
904
-							if ($search === '') {
905
-								$this->access->connection->writeToCache($cacheKey, false);
906
-							}
907
-							continue;
908
-						}
909
-						$this->access->connection->writeToCache($cacheKey, true);
910
-					}
911
-					$groupUsers[] = $uid;
912
-					break;
913
-			}
914
-		}
915
-
916
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
917
-		natsort($groupUsers);
918
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
919
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
920
-
921
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
922
-
923
-		return $groupUsers;
924
-	}
925
-
926
-	/**
927
-	 * returns the number of users in a group, who match the search term
928
-	 *
929
-	 * @param string $gid the internal group name
930
-	 * @param string $search optional, a search string
931
-	 * @return int|bool
932
-	 * @throws Exception
933
-	 * @throws ServerNotAvailableException
934
-	 */
935
-	public function countUsersInGroup($gid, $search = '') {
936
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
937
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
938
-		}
939
-
940
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
941
-		if (!$this->enabled || !$this->groupExists($gid)) {
942
-			return false;
943
-		}
944
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
945
-		if (!is_null($groupUsers)) {
946
-			return $groupUsers;
947
-		}
948
-
949
-		$groupDN = $this->access->groupname2dn($gid);
950
-		if (!$groupDN) {
951
-			// group couldn't be found, return empty result set
952
-			$this->access->connection->writeToCache($cacheKey, false);
953
-			return false;
954
-		}
955
-
956
-		$members = $this->_groupMembers($groupDN);
957
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
958
-		if (!$members && $primaryUserCount === 0) {
959
-			//in case users could not be retrieved, return empty result set
960
-			$this->access->connection->writeToCache($cacheKey, false);
961
-			return false;
962
-		}
963
-
964
-		if ($search === '') {
965
-			$groupUsers = count($members) + $primaryUserCount;
966
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
967
-			return $groupUsers;
968
-		}
969
-		$search = $this->access->escapeFilterPart($search, true);
970
-		$isMemberUid =
971
-			($this->ldapGroupMemberAssocAttr === 'memberuid' ||
972
-				$this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
973
-
974
-		//we need to apply the search filter
975
-		//alternatives that need to be checked:
976
-		//a) get all users by search filter and array_intersect them
977
-		//b) a, but only when less than 1k 10k ?k users like it is
978
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
979
-		//   and let it count.
980
-		//For now this is not important, because the only use of this method
981
-		//does not supply a search string
982
-		$groupUsers = [];
983
-		foreach ($members as $member) {
984
-			if ($isMemberUid) {
985
-				if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
986
-					//we get email addresses and need to convert them to uids
987
-					$parts = explode('@', $member);
988
-					$member = $parts[0];
989
-				}
990
-				//we got uids, need to get their DNs to 'translate' them to user names
991
-				$filter = $this->access->combineFilterWithAnd([
992
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
993
-					$this->access->getFilterPartForUserSearch($search)
994
-				]);
995
-				$ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
996
-				if (count($ldap_users) < 1) {
997
-					continue;
998
-				}
999
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
1000
-			} else {
1001
-				//we need to apply the search filter now
1002
-				if (!$this->access->readAttribute($member,
1003
-					$this->access->connection->ldapUserDisplayName,
1004
-					$this->access->getFilterPartForUserSearch($search))) {
1005
-					continue;
1006
-				}
1007
-				// dn2username will also check if the users belong to the allowed base
1008
-				if ($ncGroupId = $this->access->dn2username($member)) {
1009
-					$groupUsers[] = $ncGroupId;
1010
-				}
1011
-			}
1012
-		}
1013
-
1014
-		//and get users that have the group as primary
1015
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1016
-
1017
-		return count($groupUsers) + $primaryUsers;
1018
-	}
1019
-
1020
-	/**
1021
-	 * get a list of all groups using a paged search
1022
-	 *
1023
-	 * @param string $search
1024
-	 * @param int $limit
1025
-	 * @param int $offset
1026
-	 * @return array with group names
1027
-	 *
1028
-	 * Returns a list with all groups
1029
-	 * Uses a paged search if available to override a
1030
-	 * server side search limit.
1031
-	 * (active directory has a limit of 1000 by default)
1032
-	 * @throws Exception
1033
-	 */
1034
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1035
-		if (!$this->enabled) {
1036
-			return [];
1037
-		}
1038
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1039
-
1040
-		//Check cache before driving unnecessary searches
1041
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
1042
-		if (!is_null($ldap_groups)) {
1043
-			return $ldap_groups;
1044
-		}
1045
-
1046
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1047
-		// error. With a limit of 0, we get 0 results. So we pass null.
1048
-		if ($limit <= 0) {
1049
-			$limit = null;
1050
-		}
1051
-		$filter = $this->access->combineFilterWithAnd([
1052
-			$this->access->connection->ldapGroupFilter,
1053
-			$this->access->getFilterPartForGroupSearch($search)
1054
-		]);
1055
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
1056
-			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1057
-			$limit,
1058
-			$offset);
1059
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1060
-
1061
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1062
-		return $ldap_groups;
1063
-	}
1064
-
1065
-	/**
1066
-	 * check if a group exists
1067
-	 *
1068
-	 * @param string $gid
1069
-	 * @return bool
1070
-	 * @throws ServerNotAvailableException
1071
-	 */
1072
-	public function groupExists($gid) {
1073
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1074
-		if (!is_null($groupExists)) {
1075
-			return (bool)$groupExists;
1076
-		}
1077
-
1078
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1079
-		//only, requires more checking.
1080
-		$dn = $this->access->groupname2dn($gid);
1081
-		if (!$dn) {
1082
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1083
-			return false;
1084
-		}
1085
-
1086
-		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1087
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1088
-			return false;
1089
-		}
1090
-
1091
-		//if group really still exists, we will be able to read its objectClass
1092
-		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1093
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1094
-			return false;
1095
-		}
1096
-
1097
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1098
-		return true;
1099
-	}
1100
-
1101
-	/**
1102
-	 * @throws ServerNotAvailableException
1103
-	 * @throws Exception
1104
-	 */
1105
-	protected function filterValidGroups(array $listOfGroups): array {
1106
-		$validGroupDNs = [];
1107
-		foreach ($listOfGroups as $key => $item) {
1108
-			$dn = is_string($item) ? $item : $item['dn'][0];
1109
-			$gid = $this->access->dn2groupname($dn);
1110
-			if (!$gid) {
1111
-				continue;
1112
-			}
1113
-			if ($this->groupExists($gid)) {
1114
-				$validGroupDNs[$key] = $item;
1115
-			}
1116
-		}
1117
-		return $validGroupDNs;
1118
-	}
1119
-
1120
-	/**
1121
-	 * Check if backend implements actions
1122
-	 *
1123
-	 * @param int $actions bitwise-or'ed actions
1124
-	 * @return boolean
1125
-	 *
1126
-	 * Returns the supported actions as int to be
1127
-	 * compared with GroupInterface::CREATE_GROUP etc.
1128
-	 */
1129
-	public function implementsActions($actions) {
1130
-		return (bool)((GroupInterface::COUNT_USERS |
1131
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1132
-	}
1133
-
1134
-	/**
1135
-	 * Return access for LDAP interaction.
1136
-	 *
1137
-	 * @return Access instance of Access for LDAP interaction
1138
-	 */
1139
-	public function getLDAPAccess($gid) {
1140
-		return $this->access;
1141
-	}
1142
-
1143
-	/**
1144
-	 * create a group
1145
-	 *
1146
-	 * @param string $gid
1147
-	 * @return bool
1148
-	 * @throws Exception
1149
-	 * @throws ServerNotAvailableException
1150
-	 */
1151
-	public function createGroup($gid) {
1152
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1153
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1154
-				//updates group mapping
1155
-				$uuid = $this->access->getUUID($dn, false);
1156
-				if (is_string($uuid)) {
1157
-					$this->access->mapAndAnnounceIfApplicable(
1158
-						$this->access->getGroupMapper(),
1159
-						$dn,
1160
-						$gid,
1161
-						$uuid,
1162
-						false
1163
-					);
1164
-					$this->access->cacheGroupExists($gid);
1165
-				}
1166
-			}
1167
-			return $dn != null;
1168
-		}
1169
-		throw new Exception('Could not create group in LDAP backend.');
1170
-	}
1171
-
1172
-	/**
1173
-	 * delete a group
1174
-	 *
1175
-	 * @param string $gid gid of the group to delete
1176
-	 * @return bool
1177
-	 * @throws Exception
1178
-	 */
1179
-	public function deleteGroup($gid) {
1180
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1181
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1182
-				#delete group in nextcloud internal db
1183
-				$this->access->getGroupMapper()->unmap($gid);
1184
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1185
-			}
1186
-			return $ret;
1187
-		}
1188
-		throw new Exception('Could not delete group in LDAP backend.');
1189
-	}
1190
-
1191
-	/**
1192
-	 * Add a user to a group
1193
-	 *
1194
-	 * @param string $uid Name of the user to add to group
1195
-	 * @param string $gid Name of the group in which add the user
1196
-	 * @return bool
1197
-	 * @throws Exception
1198
-	 */
1199
-	public function addToGroup($uid, $gid) {
1200
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1201
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1202
-				$this->access->connection->clearCache();
1203
-				unset($this->cachedGroupMembers[$gid]);
1204
-			}
1205
-			return $ret;
1206
-		}
1207
-		throw new Exception('Could not add user to group in LDAP backend.');
1208
-	}
1209
-
1210
-	/**
1211
-	 * Removes a user from a group
1212
-	 *
1213
-	 * @param string $uid Name of the user to remove from group
1214
-	 * @param string $gid Name of the group from which remove the user
1215
-	 * @return bool
1216
-	 * @throws Exception
1217
-	 */
1218
-	public function removeFromGroup($uid, $gid) {
1219
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1220
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1221
-				$this->access->connection->clearCache();
1222
-				unset($this->cachedGroupMembers[$gid]);
1223
-			}
1224
-			return $ret;
1225
-		}
1226
-		throw new Exception('Could not remove user from group in LDAP backend.');
1227
-	}
1228
-
1229
-	/**
1230
-	 * Gets group details
1231
-	 *
1232
-	 * @param string $gid Name of the group
1233
-	 * @return array|false
1234
-	 * @throws Exception
1235
-	 */
1236
-	public function getGroupDetails($gid) {
1237
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1238
-			return $this->groupPluginManager->getGroupDetails($gid);
1239
-		}
1240
-		throw new Exception('Could not get group details in LDAP backend.');
1241
-	}
1242
-
1243
-	/**
1244
-	 * Return LDAP connection resource from a cloned connection.
1245
-	 * The cloned connection needs to be closed manually.
1246
-	 * of the current access.
1247
-	 *
1248
-	 * @param string $gid
1249
-	 * @return resource of the LDAP connection
1250
-	 * @throws ServerNotAvailableException
1251
-	 */
1252
-	public function getNewLDAPConnection($gid) {
1253
-		$connection = clone $this->access->getConnection();
1254
-		return $connection->getConnectionResource();
1255
-	}
1256
-
1257
-	/**
1258
-	 * @throws ServerNotAvailableException
1259
-	 */
1260
-	public function getDisplayName(string $gid): string {
1261
-		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1262
-			return $this->groupPluginManager->getDisplayName($gid);
1263
-		}
1264
-
1265
-		$cacheKey = 'group_getDisplayName' . $gid;
1266
-		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1267
-			return $displayName;
1268
-		}
1269
-
1270
-		$displayName = $this->access->readAttribute(
1271
-			$this->access->groupname2dn($gid),
1272
-			$this->access->connection->ldapGroupDisplayName);
1273
-
1274
-		if ($displayName && (count($displayName) > 0)) {
1275
-			$displayName = $displayName[0];
1276
-			$this->access->connection->writeToCache($cacheKey, $displayName);
1277
-			return $displayName;
1278
-		}
1279
-
1280
-		return '';
1281
-	}
57
+    protected $enabled = false;
58
+
59
+    /** @var string[] $cachedGroupMembers array of users with gid as key */
60
+    protected $cachedGroupMembers;
61
+    /** @var string[] $cachedGroupsByMember array of groups with uid as key */
62
+    protected $cachedGroupsByMember;
63
+    /** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
64
+    protected $cachedNestedGroups;
65
+    /** @var GroupPluginManager */
66
+    protected $groupPluginManager;
67
+    /** @var ILogger */
68
+    protected $logger;
69
+
70
+    /**
71
+     * @var string $ldapGroupMemberAssocAttr contains the LDAP setting (in lower case) with the same name
72
+     */
73
+    protected $ldapGroupMemberAssocAttr;
74
+
75
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
76
+        parent::__construct($access);
77
+        $filter = $this->access->connection->ldapGroupFilter;
78
+        $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
79
+        if (!empty($filter) && !empty($gAssoc)) {
80
+            $this->enabled = true;
81
+        }
82
+
83
+        $this->cachedGroupMembers = new CappedMemoryCache();
84
+        $this->cachedGroupsByMember = new CappedMemoryCache();
85
+        $this->cachedNestedGroups = new CappedMemoryCache();
86
+        $this->groupPluginManager = $groupPluginManager;
87
+        $this->logger = OC::$server->getLogger();
88
+        $this->ldapGroupMemberAssocAttr = strtolower($gAssoc);
89
+    }
90
+
91
+    /**
92
+     * is user in group?
93
+     *
94
+     * @param string $uid uid of the user
95
+     * @param string $gid gid of the group
96
+     * @return bool
97
+     * @throws Exception
98
+     * @throws ServerNotAvailableException
99
+     */
100
+    public function inGroup($uid, $gid) {
101
+        if (!$this->enabled) {
102
+            return false;
103
+        }
104
+        $cacheKey = 'inGroup' . $uid . ':' . $gid;
105
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
106
+        if (!is_null($inGroup)) {
107
+            return (bool)$inGroup;
108
+        }
109
+
110
+        $userDN = $this->access->username2dn($uid);
111
+
112
+        if (isset($this->cachedGroupMembers[$gid])) {
113
+            return in_array($userDN, $this->cachedGroupMembers[$gid]);
114
+        }
115
+
116
+        $cacheKeyMembers = 'inGroup-members:' . $gid;
117
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
118
+        if (!is_null($members)) {
119
+            $this->cachedGroupMembers[$gid] = $members;
120
+            $isInGroup = in_array($userDN, $members, true);
121
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
122
+            return $isInGroup;
123
+        }
124
+
125
+        $groupDN = $this->access->groupname2dn($gid);
126
+        // just in case
127
+        if (!$groupDN || !$userDN) {
128
+            $this->access->connection->writeToCache($cacheKey, false);
129
+            return false;
130
+        }
131
+
132
+        //check primary group first
133
+        if ($gid === $this->getUserPrimaryGroup($userDN)) {
134
+            $this->access->connection->writeToCache($cacheKey, true);
135
+            return true;
136
+        }
137
+
138
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
139
+        $members = $this->_groupMembers($groupDN);
140
+        if (!is_array($members) || count($members) === 0) {
141
+            $this->access->connection->writeToCache($cacheKey, false);
142
+            return false;
143
+        }
144
+
145
+        //extra work if we don't get back user DNs
146
+        switch ($this->ldapGroupMemberAssocAttr) {
147
+            case 'memberuid':
148
+            case 'zimbramailforwardingaddress':
149
+                $requestAttributes = $this->access->userManager->getAttributes(true);
150
+                $dns = [];
151
+                $filterParts = [];
152
+                $bytes = 0;
153
+                foreach ($members as $mid) {
154
+                    if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
155
+                        $parts = explode('@', $mid); //making sure we get only the uid
156
+                        $mid = $parts[0];
157
+                    }
158
+                    $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
159
+                    $filterParts[] = $filter;
160
+                    $bytes += strlen($filter);
161
+                    if ($bytes >= 9000000) {
162
+                        // AD has a default input buffer of 10 MB, we do not want
163
+                        // to take even the chance to exceed it
164
+                        $filter = $this->access->combineFilterWithOr($filterParts);
165
+                        $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
166
+                        $bytes = 0;
167
+                        $filterParts = [];
168
+                        $dns = array_merge($dns, $users);
169
+                    }
170
+                }
171
+                if (count($filterParts) > 0) {
172
+                    $filter = $this->access->combineFilterWithOr($filterParts);
173
+                    $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
174
+                    $dns = array_merge($dns, $users);
175
+                }
176
+                $members = $dns;
177
+                break;
178
+        }
179
+
180
+        $isInGroup = in_array($userDN, $members);
181
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
182
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
183
+        $this->cachedGroupMembers[$gid] = $members;
184
+
185
+        return $isInGroup;
186
+    }
187
+
188
+    /**
189
+     * For a group that has user membership defined by an LDAP search url
190
+     * attribute returns the users that match the search url otherwise returns
191
+     * an empty array.
192
+     *
193
+     * @throws ServerNotAvailableException
194
+     */
195
+    public function getDynamicGroupMembers(string $dnGroup): array {
196
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
197
+
198
+        if (empty($dynamicGroupMemberURL)) {
199
+            return [];
200
+        }
201
+
202
+        $dynamicMembers = [];
203
+        $memberURLs = $this->access->readAttribute(
204
+            $dnGroup,
205
+            $dynamicGroupMemberURL,
206
+            $this->access->connection->ldapGroupFilter
207
+        );
208
+        if ($memberURLs !== false) {
209
+            // this group has the 'memberURL' attribute so this is a dynamic group
210
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
211
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
212
+            $pos = strpos($memberURLs[0], '(');
213
+            if ($pos !== false) {
214
+                $memberUrlFilter = substr($memberURLs[0], $pos);
215
+                $foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
216
+                $dynamicMembers = [];
217
+                foreach ($foundMembers as $value) {
218
+                    $dynamicMembers[$value['dn'][0]] = 1;
219
+                }
220
+            } else {
221
+                $this->logger->debug('No search filter found on member url of group {dn}',
222
+                    [
223
+                        'app' => 'user_ldap',
224
+                        'dn' => $dnGroup,
225
+                    ]
226
+                );
227
+            }
228
+        }
229
+        return $dynamicMembers;
230
+    }
231
+
232
+    /**
233
+     * @throws ServerNotAvailableException
234
+     */
235
+    private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
236
+        if ($seen === null) {
237
+            $seen = [];
238
+        }
239
+        $allMembers = [];
240
+        if (array_key_exists($dnGroup, $seen)) {
241
+            return [];
242
+        }
243
+        // used extensively in cron job, caching makes sense for nested groups
244
+        $cacheKey = '_groupMembers' . $dnGroup;
245
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
246
+        if ($groupMembers !== null) {
247
+            return $groupMembers;
248
+        }
249
+        $seen[$dnGroup] = 1;
250
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
251
+        if (is_array($members)) {
252
+            $fetcher = function ($memberDN, &$seen) {
253
+                return $this->_groupMembers($memberDN, $seen);
254
+            };
255
+            $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
256
+        }
257
+
258
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
259
+
260
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
261
+        return $allMembers;
262
+    }
263
+
264
+    /**
265
+     * @throws ServerNotAvailableException
266
+     */
267
+    private function _getGroupDNsFromMemberOf(string $dn): array {
268
+        $groups = $this->access->readAttribute($dn, 'memberOf');
269
+        if (!is_array($groups)) {
270
+            return [];
271
+        }
272
+
273
+        $fetcher = function ($groupDN) {
274
+            if (isset($this->cachedNestedGroups[$groupDN])) {
275
+                $nestedGroups = $this->cachedNestedGroups[$groupDN];
276
+            } else {
277
+                $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
278
+                if (!is_array($nestedGroups)) {
279
+                    $nestedGroups = [];
280
+                }
281
+                $this->cachedNestedGroups[$groupDN] = $nestedGroups;
282
+            }
283
+            return $nestedGroups;
284
+        };
285
+
286
+        $groups = $this->walkNestedGroups($dn, $fetcher, $groups);
287
+        return $this->filterValidGroups($groups);
288
+    }
289
+
290
+    private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
291
+        $nesting = (int)$this->access->connection->ldapNestedGroups;
292
+        // depending on the input, we either have a list of DNs or a list of LDAP records
293
+        // also, the output expects either DNs or records. Testing the first element should suffice.
294
+        $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
295
+
296
+        if ($nesting !== 1) {
297
+            if ($recordMode) {
298
+                // the keys are numeric, but should hold the DN
299
+                return array_reduce($list, function ($transformed, $record) use ($dn) {
300
+                    if ($record['dn'][0] != $dn) {
301
+                        $transformed[$record['dn'][0]] = $record;
302
+                    }
303
+                    return $transformed;
304
+                }, []);
305
+            }
306
+            return $list;
307
+        }
308
+
309
+        $seen = [];
310
+        while ($record = array_pop($list)) {
311
+            $recordDN = $recordMode ? $record['dn'][0] : $record;
312
+            if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
313
+                // Prevent loops
314
+                continue;
315
+            }
316
+            $fetched = $fetcher($record, $seen);
317
+            $list = array_merge($list, $fetched);
318
+            $seen[$recordDN] = $record;
319
+        }
320
+
321
+        return $recordMode ? $seen : array_keys($seen);
322
+    }
323
+
324
+    /**
325
+     * translates a gidNumber into an ownCloud internal name
326
+     *
327
+     * @return string|bool
328
+     * @throws Exception
329
+     * @throws ServerNotAvailableException
330
+     */
331
+    public function gidNumber2Name(string $gid, string $dn) {
332
+        $cacheKey = 'gidNumberToName' . $gid;
333
+        $groupName = $this->access->connection->getFromCache($cacheKey);
334
+        if (!is_null($groupName) && isset($groupName)) {
335
+            return $groupName;
336
+        }
337
+
338
+        //we need to get the DN from LDAP
339
+        $filter = $this->access->combineFilterWithAnd([
340
+            $this->access->connection->ldapGroupFilter,
341
+            'objectClass=posixGroup',
342
+            $this->access->connection->ldapGidNumber . '=' . $gid
343
+        ]);
344
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
345
+    }
346
+
347
+    /**
348
+     * @throws ServerNotAvailableException
349
+     * @throws Exception
350
+     */
351
+    private function getNameOfGroup(string $filter, string $cacheKey) {
352
+        $result = $this->access->searchGroups($filter, ['dn'], 1);
353
+        if (empty($result)) {
354
+            return null;
355
+        }
356
+        $dn = $result[0]['dn'][0];
357
+
358
+        //and now the group name
359
+        //NOTE once we have separate Nextcloud group IDs and group names we can
360
+        //directly read the display name attribute instead of the DN
361
+        $name = $this->access->dn2groupname($dn);
362
+
363
+        $this->access->connection->writeToCache($cacheKey, $name);
364
+
365
+        return $name;
366
+    }
367
+
368
+    /**
369
+     * returns the entry's gidNumber
370
+     *
371
+     * @return string|bool
372
+     * @throws ServerNotAvailableException
373
+     */
374
+    private function getEntryGidNumber(string $dn, string $attribute) {
375
+        $value = $this->access->readAttribute($dn, $attribute);
376
+        if (is_array($value) && !empty($value)) {
377
+            return $value[0];
378
+        }
379
+        return false;
380
+    }
381
+
382
+    /**
383
+     * @return string|bool
384
+     * @throws ServerNotAvailableException
385
+     */
386
+    public function getGroupGidNumber(string $dn) {
387
+        return $this->getEntryGidNumber($dn, 'gidNumber');
388
+    }
389
+
390
+    /**
391
+     * returns the user's gidNumber
392
+     *
393
+     * @return string|bool
394
+     * @throws ServerNotAvailableException
395
+     */
396
+    public function getUserGidNumber(string $dn) {
397
+        $gidNumber = false;
398
+        if ($this->access->connection->hasGidNumber) {
399
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
400
+            if ($gidNumber === false) {
401
+                $this->access->connection->hasGidNumber = false;
402
+            }
403
+        }
404
+        return $gidNumber;
405
+    }
406
+
407
+    /**
408
+     * @throws ServerNotAvailableException
409
+     * @throws Exception
410
+     */
411
+    private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
412
+        $groupID = $this->getGroupGidNumber($groupDN);
413
+        if ($groupID === false) {
414
+            throw new Exception('Not a valid group');
415
+        }
416
+
417
+        $filterParts = [];
418
+        $filterParts[] = $this->access->getFilterForUserCount();
419
+        if ($search !== '') {
420
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
421
+        }
422
+        $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
423
+
424
+        return $this->access->combineFilterWithAnd($filterParts);
425
+    }
426
+
427
+    /**
428
+     * returns a list of users that have the given group as gid number
429
+     *
430
+     * @throws ServerNotAvailableException
431
+     */
432
+    public function getUsersInGidNumber(
433
+        string $groupDN,
434
+        string $search = '',
435
+        ?int $limit = -1,
436
+        ?int $offset = 0
437
+    ): array {
438
+        try {
439
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
440
+            $users = $this->access->fetchListOfUsers(
441
+                $filter,
442
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
443
+                $limit,
444
+                $offset
445
+            );
446
+            return $this->access->nextcloudUserNames($users);
447
+        } catch (ServerNotAvailableException $e) {
448
+            throw $e;
449
+        } catch (Exception $e) {
450
+            return [];
451
+        }
452
+    }
453
+
454
+    /**
455
+     * @throws ServerNotAvailableException
456
+     * @return bool
457
+     */
458
+    public function getUserGroupByGid(string $dn) {
459
+        $groupID = $this->getUserGidNumber($dn);
460
+        if ($groupID !== false) {
461
+            $groupName = $this->gidNumber2Name($groupID, $dn);
462
+            if ($groupName !== false) {
463
+                return $groupName;
464
+            }
465
+        }
466
+
467
+        return false;
468
+    }
469
+
470
+    /**
471
+     * translates a primary group ID into an Nextcloud internal name
472
+     *
473
+     * @return string|bool
474
+     * @throws Exception
475
+     * @throws ServerNotAvailableException
476
+     */
477
+    public function primaryGroupID2Name(string $gid, string $dn) {
478
+        $cacheKey = 'primaryGroupIDtoName';
479
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
480
+        if (!is_null($groupNames) && isset($groupNames[$gid])) {
481
+            return $groupNames[$gid];
482
+        }
483
+
484
+        $domainObjectSid = $this->access->getSID($dn);
485
+        if ($domainObjectSid === false) {
486
+            return false;
487
+        }
488
+
489
+        //we need to get the DN from LDAP
490
+        $filter = $this->access->combineFilterWithAnd([
491
+            $this->access->connection->ldapGroupFilter,
492
+            'objectsid=' . $domainObjectSid . '-' . $gid
493
+        ]);
494
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
495
+    }
496
+
497
+    /**
498
+     * returns the entry's primary group ID
499
+     *
500
+     * @return string|bool
501
+     * @throws ServerNotAvailableException
502
+     */
503
+    private function getEntryGroupID(string $dn, string $attribute) {
504
+        $value = $this->access->readAttribute($dn, $attribute);
505
+        if (is_array($value) && !empty($value)) {
506
+            return $value[0];
507
+        }
508
+        return false;
509
+    }
510
+
511
+    /**
512
+     * @return string|bool
513
+     * @throws ServerNotAvailableException
514
+     */
515
+    public function getGroupPrimaryGroupID(string $dn) {
516
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
517
+    }
518
+
519
+    /**
520
+     * @return string|bool
521
+     * @throws ServerNotAvailableException
522
+     */
523
+    public function getUserPrimaryGroupIDs(string $dn) {
524
+        $primaryGroupID = false;
525
+        if ($this->access->connection->hasPrimaryGroups) {
526
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
527
+            if ($primaryGroupID === false) {
528
+                $this->access->connection->hasPrimaryGroups = false;
529
+            }
530
+        }
531
+        return $primaryGroupID;
532
+    }
533
+
534
+    /**
535
+     * @throws Exception
536
+     * @throws ServerNotAvailableException
537
+     */
538
+    private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
539
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
540
+        if ($groupID === false) {
541
+            throw new Exception('Not a valid group');
542
+        }
543
+
544
+        $filterParts = [];
545
+        $filterParts[] = $this->access->getFilterForUserCount();
546
+        if ($search !== '') {
547
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
548
+        }
549
+        $filterParts[] = 'primaryGroupID=' . $groupID;
550
+
551
+        return $this->access->combineFilterWithAnd($filterParts);
552
+    }
553
+
554
+    /**
555
+     * @throws ServerNotAvailableException
556
+     */
557
+    public function getUsersInPrimaryGroup(
558
+        string $groupDN,
559
+        string $search = '',
560
+        ?int $limit = -1,
561
+        ?int $offset = 0
562
+    ): array {
563
+        try {
564
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
565
+            $users = $this->access->fetchListOfUsers(
566
+                $filter,
567
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
568
+                $limit,
569
+                $offset
570
+            );
571
+            return $this->access->nextcloudUserNames($users);
572
+        } catch (ServerNotAvailableException $e) {
573
+            throw $e;
574
+        } catch (Exception $e) {
575
+            return [];
576
+        }
577
+    }
578
+
579
+    /**
580
+     * @throws ServerNotAvailableException
581
+     */
582
+    public function countUsersInPrimaryGroup(
583
+        string $groupDN,
584
+        string $search = '',
585
+        int $limit = -1,
586
+        int $offset = 0
587
+    ): int {
588
+        try {
589
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
590
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
591
+            return (int)$users;
592
+        } catch (ServerNotAvailableException $e) {
593
+            throw $e;
594
+        } catch (Exception $e) {
595
+            return 0;
596
+        }
597
+    }
598
+
599
+    /**
600
+     * @return string|bool
601
+     * @throws ServerNotAvailableException
602
+     */
603
+    public function getUserPrimaryGroup(string $dn) {
604
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
605
+        if ($groupID !== false) {
606
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
607
+            if ($groupName !== false) {
608
+                return $groupName;
609
+            }
610
+        }
611
+
612
+        return false;
613
+    }
614
+
615
+    /**
616
+     * This function fetches all groups a user belongs to. It does not check
617
+     * if the user exists at all.
618
+     *
619
+     * This function includes groups based on dynamic group membership.
620
+     *
621
+     * @param string $uid Name of the user
622
+     * @return array with group names
623
+     * @throws Exception
624
+     * @throws ServerNotAvailableException
625
+     */
626
+    public function getUserGroups($uid) {
627
+        if (!$this->enabled) {
628
+            return [];
629
+        }
630
+        $cacheKey = 'getUserGroups' . $uid;
631
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
632
+        if (!is_null($userGroups)) {
633
+            return $userGroups;
634
+        }
635
+        $userDN = $this->access->username2dn($uid);
636
+        if (!$userDN) {
637
+            $this->access->connection->writeToCache($cacheKey, []);
638
+            return [];
639
+        }
640
+
641
+        $groups = [];
642
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
643
+        $gidGroupName = $this->getUserGroupByGid($userDN);
644
+
645
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
646
+
647
+        if (!empty($dynamicGroupMemberURL)) {
648
+            // look through dynamic groups to add them to the result array if needed
649
+            $groupsToMatch = $this->access->fetchListOfGroups(
650
+                $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
651
+            foreach ($groupsToMatch as $dynamicGroup) {
652
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
653
+                    continue;
654
+                }
655
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
656
+                if ($pos !== false) {
657
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
658
+                    // apply filter via ldap search to see if this user is in this
659
+                    // dynamic group
660
+                    $userMatch = $this->access->readAttribute(
661
+                        $userDN,
662
+                        $this->access->connection->ldapUserDisplayName,
663
+                        $memberUrlFilter
664
+                    );
665
+                    if ($userMatch !== false) {
666
+                        // match found so this user is in this group
667
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
668
+                        if (is_string($groupName)) {
669
+                            // be sure to never return false if the dn could not be
670
+                            // resolved to a name, for whatever reason.
671
+                            $groups[] = $groupName;
672
+                        }
673
+                    }
674
+                } else {
675
+                    $this->logger->debug('No search filter found on member url of group {dn}',
676
+                        [
677
+                            'app' => 'user_ldap',
678
+                            'dn' => $dynamicGroup,
679
+                        ]
680
+                    );
681
+                }
682
+            }
683
+        }
684
+
685
+        // if possible, read out membership via memberOf. It's far faster than
686
+        // performing a search, which still is a fallback later.
687
+        // memberof doesn't support memberuid, so skip it here.
688
+        if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
689
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
690
+            && $this->ldapGroupMemberAssocAttr !== 'memberuid'
691
+            && $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
692
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
693
+            if (is_array($groupDNs)) {
694
+                foreach ($groupDNs as $dn) {
695
+                    $groupName = $this->access->dn2groupname($dn);
696
+                    if (is_string($groupName)) {
697
+                        // be sure to never return false if the dn could not be
698
+                        // resolved to a name, for whatever reason.
699
+                        $groups[] = $groupName;
700
+                    }
701
+                }
702
+            }
703
+
704
+            if ($primaryGroup !== false) {
705
+                $groups[] = $primaryGroup;
706
+            }
707
+            if ($gidGroupName !== false) {
708
+                $groups[] = $gidGroupName;
709
+            }
710
+            $this->access->connection->writeToCache($cacheKey, $groups);
711
+            return $groups;
712
+        }
713
+
714
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
715
+        switch ($this->ldapGroupMemberAssocAttr) {
716
+            case 'uniquemember':
717
+            case 'member':
718
+                $uid = $userDN;
719
+                break;
720
+
721
+            case 'memberuid':
722
+            case 'zimbramailforwardingaddress':
723
+                $result = $this->access->readAttribute($userDN, 'uid');
724
+                if ($result === false) {
725
+                    $this->logger->debug('No uid attribute found for DN {dn} on {host}',
726
+                        [
727
+                            'app' => 'user_ldap',
728
+                            'dn' => $userDN,
729
+                            'host' => $this->access->connection->ldapHost,
730
+                        ]
731
+                    );
732
+                    $uid = false;
733
+                } else {
734
+                    $uid = $result[0];
735
+                }
736
+                break;
737
+
738
+            default:
739
+                // just in case
740
+                $uid = $userDN;
741
+                break;
742
+        }
743
+
744
+        if ($uid !== false) {
745
+            if (isset($this->cachedGroupsByMember[$uid])) {
746
+                $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
747
+            } else {
748
+                $groupsByMember = array_values($this->getGroupsByMember($uid));
749
+                $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
750
+                $this->cachedGroupsByMember[$uid] = $groupsByMember;
751
+                $groups = array_merge($groups, $groupsByMember);
752
+            }
753
+        }
754
+
755
+        if ($primaryGroup !== false) {
756
+            $groups[] = $primaryGroup;
757
+        }
758
+        if ($gidGroupName !== false) {
759
+            $groups[] = $gidGroupName;
760
+        }
761
+
762
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
763
+        $this->access->connection->writeToCache($cacheKey, $groups);
764
+
765
+        return $groups;
766
+    }
767
+
768
+    /**
769
+     * @throws ServerNotAvailableException
770
+     */
771
+    private function getGroupsByMember(string $dn, array &$seen = null): array {
772
+        if ($seen === null) {
773
+            $seen = [];
774
+        }
775
+        if (array_key_exists($dn, $seen)) {
776
+            // avoid loops
777
+            return [];
778
+        }
779
+        $allGroups = [];
780
+        $seen[$dn] = true;
781
+        $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
782
+
783
+        if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
784
+            //in this case the member entries are email addresses
785
+            $filter .= '@*';
786
+        }
787
+
788
+        $groups = $this->access->fetchListOfGroups($filter,
789
+            [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
790
+        if (is_array($groups)) {
791
+            $fetcher = function ($dn, &$seen) {
792
+                if (is_array($dn) && isset($dn['dn'][0])) {
793
+                    $dn = $dn['dn'][0];
794
+                }
795
+                return $this->getGroupsByMember($dn, $seen);
796
+            };
797
+
798
+            if (empty($dn)) {
799
+                $dn = "";
800
+            }
801
+
802
+            $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
803
+        }
804
+        $visibleGroups = $this->filterValidGroups($allGroups);
805
+        return array_intersect_key($allGroups, $visibleGroups);
806
+    }
807
+
808
+    /**
809
+     * get a list of all users in a group
810
+     *
811
+     * @param string $gid
812
+     * @param string $search
813
+     * @param int $limit
814
+     * @param int $offset
815
+     * @return array with user ids
816
+     * @throws Exception
817
+     * @throws ServerNotAvailableException
818
+     */
819
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
820
+        if (!$this->enabled) {
821
+            return [];
822
+        }
823
+        if (!$this->groupExists($gid)) {
824
+            return [];
825
+        }
826
+        $search = $this->access->escapeFilterPart($search, true);
827
+        $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
828
+        // check for cache of the exact query
829
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
830
+        if (!is_null($groupUsers)) {
831
+            return $groupUsers;
832
+        }
833
+
834
+        if ($limit === -1) {
835
+            $limit = null;
836
+        }
837
+        // check for cache of the query without limit and offset
838
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
839
+        if (!is_null($groupUsers)) {
840
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
841
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
842
+            return $groupUsers;
843
+        }
844
+
845
+        $groupDN = $this->access->groupname2dn($gid);
846
+        if (!$groupDN) {
847
+            // group couldn't be found, return empty resultset
848
+            $this->access->connection->writeToCache($cacheKey, []);
849
+            return [];
850
+        }
851
+
852
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
853
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
854
+        $members = $this->_groupMembers($groupDN);
855
+        if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
856
+            //in case users could not be retrieved, return empty result set
857
+            $this->access->connection->writeToCache($cacheKey, []);
858
+            return [];
859
+        }
860
+
861
+        $groupUsers = [];
862
+        $attrs = $this->access->userManager->getAttributes(true);
863
+        foreach ($members as $member) {
864
+            switch ($this->ldapGroupMemberAssocAttr) {
865
+                case 'zimbramailforwardingaddress':
866
+                    //we get email addresses and need to convert them to uids
867
+                    $parts = explode('@', $member);
868
+                    $member = $parts[0];
869
+                    //no break needed because we just needed to remove the email part and now we have uids
870
+                case 'memberuid':
871
+                    //we got uids, need to get their DNs to 'translate' them to user names
872
+                    $filter = $this->access->combineFilterWithAnd([
873
+                        str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
874
+                        $this->access->combineFilterWithAnd([
875
+                            $this->access->getFilterPartForUserSearch($search),
876
+                            $this->access->connection->ldapUserFilter
877
+                        ])
878
+                    ]);
879
+                    $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
880
+                    if (count($ldap_users) < 1) {
881
+                        continue;
882
+                    }
883
+                    $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
884
+                    break;
885
+                default:
886
+                    //we got DNs, check if we need to filter by search or we can give back all of them
887
+                    $uid = $this->access->dn2username($member);
888
+                    if (!$uid) {
889
+                        continue;
890
+                    }
891
+
892
+                    $cacheKey = 'userExistsOnLDAP' . $uid;
893
+                    $userExists = $this->access->connection->getFromCache($cacheKey);
894
+                    if ($userExists === false) {
895
+                        continue;
896
+                    }
897
+                    if ($userExists === null || $search !== '') {
898
+                        if (!$this->access->readAttribute($member,
899
+                            $this->access->connection->ldapUserDisplayName,
900
+                            $this->access->combineFilterWithAnd([
901
+                                $this->access->getFilterPartForUserSearch($search),
902
+                                $this->access->connection->ldapUserFilter
903
+                            ]))) {
904
+                            if ($search === '') {
905
+                                $this->access->connection->writeToCache($cacheKey, false);
906
+                            }
907
+                            continue;
908
+                        }
909
+                        $this->access->connection->writeToCache($cacheKey, true);
910
+                    }
911
+                    $groupUsers[] = $uid;
912
+                    break;
913
+            }
914
+        }
915
+
916
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
917
+        natsort($groupUsers);
918
+        $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
919
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
920
+
921
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
922
+
923
+        return $groupUsers;
924
+    }
925
+
926
+    /**
927
+     * returns the number of users in a group, who match the search term
928
+     *
929
+     * @param string $gid the internal group name
930
+     * @param string $search optional, a search string
931
+     * @return int|bool
932
+     * @throws Exception
933
+     * @throws ServerNotAvailableException
934
+     */
935
+    public function countUsersInGroup($gid, $search = '') {
936
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
937
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
938
+        }
939
+
940
+        $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
941
+        if (!$this->enabled || !$this->groupExists($gid)) {
942
+            return false;
943
+        }
944
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
945
+        if (!is_null($groupUsers)) {
946
+            return $groupUsers;
947
+        }
948
+
949
+        $groupDN = $this->access->groupname2dn($gid);
950
+        if (!$groupDN) {
951
+            // group couldn't be found, return empty result set
952
+            $this->access->connection->writeToCache($cacheKey, false);
953
+            return false;
954
+        }
955
+
956
+        $members = $this->_groupMembers($groupDN);
957
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
958
+        if (!$members && $primaryUserCount === 0) {
959
+            //in case users could not be retrieved, return empty result set
960
+            $this->access->connection->writeToCache($cacheKey, false);
961
+            return false;
962
+        }
963
+
964
+        if ($search === '') {
965
+            $groupUsers = count($members) + $primaryUserCount;
966
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
967
+            return $groupUsers;
968
+        }
969
+        $search = $this->access->escapeFilterPart($search, true);
970
+        $isMemberUid =
971
+            ($this->ldapGroupMemberAssocAttr === 'memberuid' ||
972
+                $this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress');
973
+
974
+        //we need to apply the search filter
975
+        //alternatives that need to be checked:
976
+        //a) get all users by search filter and array_intersect them
977
+        //b) a, but only when less than 1k 10k ?k users like it is
978
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
979
+        //   and let it count.
980
+        //For now this is not important, because the only use of this method
981
+        //does not supply a search string
982
+        $groupUsers = [];
983
+        foreach ($members as $member) {
984
+            if ($isMemberUid) {
985
+                if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
986
+                    //we get email addresses and need to convert them to uids
987
+                    $parts = explode('@', $member);
988
+                    $member = $parts[0];
989
+                }
990
+                //we got uids, need to get their DNs to 'translate' them to user names
991
+                $filter = $this->access->combineFilterWithAnd([
992
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
993
+                    $this->access->getFilterPartForUserSearch($search)
994
+                ]);
995
+                $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
996
+                if (count($ldap_users) < 1) {
997
+                    continue;
998
+                }
999
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
1000
+            } else {
1001
+                //we need to apply the search filter now
1002
+                if (!$this->access->readAttribute($member,
1003
+                    $this->access->connection->ldapUserDisplayName,
1004
+                    $this->access->getFilterPartForUserSearch($search))) {
1005
+                    continue;
1006
+                }
1007
+                // dn2username will also check if the users belong to the allowed base
1008
+                if ($ncGroupId = $this->access->dn2username($member)) {
1009
+                    $groupUsers[] = $ncGroupId;
1010
+                }
1011
+            }
1012
+        }
1013
+
1014
+        //and get users that have the group as primary
1015
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1016
+
1017
+        return count($groupUsers) + $primaryUsers;
1018
+    }
1019
+
1020
+    /**
1021
+     * get a list of all groups using a paged search
1022
+     *
1023
+     * @param string $search
1024
+     * @param int $limit
1025
+     * @param int $offset
1026
+     * @return array with group names
1027
+     *
1028
+     * Returns a list with all groups
1029
+     * Uses a paged search if available to override a
1030
+     * server side search limit.
1031
+     * (active directory has a limit of 1000 by default)
1032
+     * @throws Exception
1033
+     */
1034
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1035
+        if (!$this->enabled) {
1036
+            return [];
1037
+        }
1038
+        $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1039
+
1040
+        //Check cache before driving unnecessary searches
1041
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
1042
+        if (!is_null($ldap_groups)) {
1043
+            return $ldap_groups;
1044
+        }
1045
+
1046
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
1047
+        // error. With a limit of 0, we get 0 results. So we pass null.
1048
+        if ($limit <= 0) {
1049
+            $limit = null;
1050
+        }
1051
+        $filter = $this->access->combineFilterWithAnd([
1052
+            $this->access->connection->ldapGroupFilter,
1053
+            $this->access->getFilterPartForGroupSearch($search)
1054
+        ]);
1055
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
1056
+            [$this->access->connection->ldapGroupDisplayName, 'dn'],
1057
+            $limit,
1058
+            $offset);
1059
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1060
+
1061
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
1062
+        return $ldap_groups;
1063
+    }
1064
+
1065
+    /**
1066
+     * check if a group exists
1067
+     *
1068
+     * @param string $gid
1069
+     * @return bool
1070
+     * @throws ServerNotAvailableException
1071
+     */
1072
+    public function groupExists($gid) {
1073
+        $groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1074
+        if (!is_null($groupExists)) {
1075
+            return (bool)$groupExists;
1076
+        }
1077
+
1078
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1079
+        //only, requires more checking.
1080
+        $dn = $this->access->groupname2dn($gid);
1081
+        if (!$dn) {
1082
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1083
+            return false;
1084
+        }
1085
+
1086
+        if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1087
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1088
+            return false;
1089
+        }
1090
+
1091
+        //if group really still exists, we will be able to read its objectClass
1092
+        if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1093
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1094
+            return false;
1095
+        }
1096
+
1097
+        $this->access->connection->writeToCache('groupExists' . $gid, true);
1098
+        return true;
1099
+    }
1100
+
1101
+    /**
1102
+     * @throws ServerNotAvailableException
1103
+     * @throws Exception
1104
+     */
1105
+    protected function filterValidGroups(array $listOfGroups): array {
1106
+        $validGroupDNs = [];
1107
+        foreach ($listOfGroups as $key => $item) {
1108
+            $dn = is_string($item) ? $item : $item['dn'][0];
1109
+            $gid = $this->access->dn2groupname($dn);
1110
+            if (!$gid) {
1111
+                continue;
1112
+            }
1113
+            if ($this->groupExists($gid)) {
1114
+                $validGroupDNs[$key] = $item;
1115
+            }
1116
+        }
1117
+        return $validGroupDNs;
1118
+    }
1119
+
1120
+    /**
1121
+     * Check if backend implements actions
1122
+     *
1123
+     * @param int $actions bitwise-or'ed actions
1124
+     * @return boolean
1125
+     *
1126
+     * Returns the supported actions as int to be
1127
+     * compared with GroupInterface::CREATE_GROUP etc.
1128
+     */
1129
+    public function implementsActions($actions) {
1130
+        return (bool)((GroupInterface::COUNT_USERS |
1131
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1132
+    }
1133
+
1134
+    /**
1135
+     * Return access for LDAP interaction.
1136
+     *
1137
+     * @return Access instance of Access for LDAP interaction
1138
+     */
1139
+    public function getLDAPAccess($gid) {
1140
+        return $this->access;
1141
+    }
1142
+
1143
+    /**
1144
+     * create a group
1145
+     *
1146
+     * @param string $gid
1147
+     * @return bool
1148
+     * @throws Exception
1149
+     * @throws ServerNotAvailableException
1150
+     */
1151
+    public function createGroup($gid) {
1152
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1153
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1154
+                //updates group mapping
1155
+                $uuid = $this->access->getUUID($dn, false);
1156
+                if (is_string($uuid)) {
1157
+                    $this->access->mapAndAnnounceIfApplicable(
1158
+                        $this->access->getGroupMapper(),
1159
+                        $dn,
1160
+                        $gid,
1161
+                        $uuid,
1162
+                        false
1163
+                    );
1164
+                    $this->access->cacheGroupExists($gid);
1165
+                }
1166
+            }
1167
+            return $dn != null;
1168
+        }
1169
+        throw new Exception('Could not create group in LDAP backend.');
1170
+    }
1171
+
1172
+    /**
1173
+     * delete a group
1174
+     *
1175
+     * @param string $gid gid of the group to delete
1176
+     * @return bool
1177
+     * @throws Exception
1178
+     */
1179
+    public function deleteGroup($gid) {
1180
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1181
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1182
+                #delete group in nextcloud internal db
1183
+                $this->access->getGroupMapper()->unmap($gid);
1184
+                $this->access->connection->writeToCache("groupExists" . $gid, false);
1185
+            }
1186
+            return $ret;
1187
+        }
1188
+        throw new Exception('Could not delete group in LDAP backend.');
1189
+    }
1190
+
1191
+    /**
1192
+     * Add a user to a group
1193
+     *
1194
+     * @param string $uid Name of the user to add to group
1195
+     * @param string $gid Name of the group in which add the user
1196
+     * @return bool
1197
+     * @throws Exception
1198
+     */
1199
+    public function addToGroup($uid, $gid) {
1200
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1201
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1202
+                $this->access->connection->clearCache();
1203
+                unset($this->cachedGroupMembers[$gid]);
1204
+            }
1205
+            return $ret;
1206
+        }
1207
+        throw new Exception('Could not add user to group in LDAP backend.');
1208
+    }
1209
+
1210
+    /**
1211
+     * Removes a user from a group
1212
+     *
1213
+     * @param string $uid Name of the user to remove from group
1214
+     * @param string $gid Name of the group from which remove the user
1215
+     * @return bool
1216
+     * @throws Exception
1217
+     */
1218
+    public function removeFromGroup($uid, $gid) {
1219
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1220
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1221
+                $this->access->connection->clearCache();
1222
+                unset($this->cachedGroupMembers[$gid]);
1223
+            }
1224
+            return $ret;
1225
+        }
1226
+        throw new Exception('Could not remove user from group in LDAP backend.');
1227
+    }
1228
+
1229
+    /**
1230
+     * Gets group details
1231
+     *
1232
+     * @param string $gid Name of the group
1233
+     * @return array|false
1234
+     * @throws Exception
1235
+     */
1236
+    public function getGroupDetails($gid) {
1237
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1238
+            return $this->groupPluginManager->getGroupDetails($gid);
1239
+        }
1240
+        throw new Exception('Could not get group details in LDAP backend.');
1241
+    }
1242
+
1243
+    /**
1244
+     * Return LDAP connection resource from a cloned connection.
1245
+     * The cloned connection needs to be closed manually.
1246
+     * of the current access.
1247
+     *
1248
+     * @param string $gid
1249
+     * @return resource of the LDAP connection
1250
+     * @throws ServerNotAvailableException
1251
+     */
1252
+    public function getNewLDAPConnection($gid) {
1253
+        $connection = clone $this->access->getConnection();
1254
+        return $connection->getConnectionResource();
1255
+    }
1256
+
1257
+    /**
1258
+     * @throws ServerNotAvailableException
1259
+     */
1260
+    public function getDisplayName(string $gid): string {
1261
+        if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1262
+            return $this->groupPluginManager->getDisplayName($gid);
1263
+        }
1264
+
1265
+        $cacheKey = 'group_getDisplayName' . $gid;
1266
+        if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1267
+            return $displayName;
1268
+        }
1269
+
1270
+        $displayName = $this->access->readAttribute(
1271
+            $this->access->groupname2dn($gid),
1272
+            $this->access->connection->ldapGroupDisplayName);
1273
+
1274
+        if ($displayName && (count($displayName) > 0)) {
1275
+            $displayName = $displayName[0];
1276
+            $this->access->connection->writeToCache($cacheKey, $displayName);
1277
+            return $displayName;
1278
+        }
1279
+
1280
+        return '';
1281
+    }
1282 1282
 }
Please login to merge, or discard this patch.
Spacing   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -101,10 +101,10 @@  discard block
 block discarded – undo
101 101
 		if (!$this->enabled) {
102 102
 			return false;
103 103
 		}
104
-		$cacheKey = 'inGroup' . $uid . ':' . $gid;
104
+		$cacheKey = 'inGroup'.$uid.':'.$gid;
105 105
 		$inGroup = $this->access->connection->getFromCache($cacheKey);
106 106
 		if (!is_null($inGroup)) {
107
-			return (bool)$inGroup;
107
+			return (bool) $inGroup;
108 108
 		}
109 109
 
110 110
 		$userDN = $this->access->username2dn($uid);
@@ -113,7 +113,7 @@  discard block
 block discarded – undo
113 113
 			return in_array($userDN, $this->cachedGroupMembers[$gid]);
114 114
 		}
115 115
 
116
-		$cacheKeyMembers = 'inGroup-members:' . $gid;
116
+		$cacheKeyMembers = 'inGroup-members:'.$gid;
117 117
 		$members = $this->access->connection->getFromCache($cacheKeyMembers);
118 118
 		if (!is_null($members)) {
119 119
 			$this->cachedGroupMembers[$gid] = $members;
@@ -241,7 +241,7 @@  discard block
 block discarded – undo
241 241
 			return [];
242 242
 		}
243 243
 		// used extensively in cron job, caching makes sense for nested groups
244
-		$cacheKey = '_groupMembers' . $dnGroup;
244
+		$cacheKey = '_groupMembers'.$dnGroup;
245 245
 		$groupMembers = $this->access->connection->getFromCache($cacheKey);
246 246
 		if ($groupMembers !== null) {
247 247
 			return $groupMembers;
@@ -249,7 +249,7 @@  discard block
 block discarded – undo
249 249
 		$seen[$dnGroup] = 1;
250 250
 		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
251 251
 		if (is_array($members)) {
252
-			$fetcher = function ($memberDN, &$seen) {
252
+			$fetcher = function($memberDN, &$seen) {
253 253
 				return $this->_groupMembers($memberDN, $seen);
254 254
 			};
255 255
 			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
@@ -270,7 +270,7 @@  discard block
 block discarded – undo
270 270
 			return [];
271 271
 		}
272 272
 
273
-		$fetcher = function ($groupDN) {
273
+		$fetcher = function($groupDN) {
274 274
 			if (isset($this->cachedNestedGroups[$groupDN])) {
275 275
 				$nestedGroups = $this->cachedNestedGroups[$groupDN];
276 276
 			} else {
@@ -288,7 +288,7 @@  discard block
 block discarded – undo
288 288
 	}
289 289
 
290 290
 	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
291
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
291
+		$nesting = (int) $this->access->connection->ldapNestedGroups;
292 292
 		// depending on the input, we either have a list of DNs or a list of LDAP records
293 293
 		// also, the output expects either DNs or records. Testing the first element should suffice.
294 294
 		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
@@ -296,7 +296,7 @@  discard block
 block discarded – undo
296 296
 		if ($nesting !== 1) {
297 297
 			if ($recordMode) {
298 298
 				// the keys are numeric, but should hold the DN
299
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
299
+				return array_reduce($list, function($transformed, $record) use ($dn) {
300 300
 					if ($record['dn'][0] != $dn) {
301 301
 						$transformed[$record['dn'][0]] = $record;
302 302
 					}
@@ -329,7 +329,7 @@  discard block
 block discarded – undo
329 329
 	 * @throws ServerNotAvailableException
330 330
 	 */
331 331
 	public function gidNumber2Name(string $gid, string $dn) {
332
-		$cacheKey = 'gidNumberToName' . $gid;
332
+		$cacheKey = 'gidNumberToName'.$gid;
333 333
 		$groupName = $this->access->connection->getFromCache($cacheKey);
334 334
 		if (!is_null($groupName) && isset($groupName)) {
335 335
 			return $groupName;
@@ -339,7 +339,7 @@  discard block
 block discarded – undo
339 339
 		$filter = $this->access->combineFilterWithAnd([
340 340
 			$this->access->connection->ldapGroupFilter,
341 341
 			'objectClass=posixGroup',
342
-			$this->access->connection->ldapGidNumber . '=' . $gid
342
+			$this->access->connection->ldapGidNumber.'='.$gid
343 343
 		]);
344 344
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
345 345
 	}
@@ -419,7 +419,7 @@  discard block
 block discarded – undo
419 419
 		if ($search !== '') {
420 420
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
421 421
 		}
422
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
422
+		$filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID;
423 423
 
424 424
 		return $this->access->combineFilterWithAnd($filterParts);
425 425
 	}
@@ -489,7 +489,7 @@  discard block
 block discarded – undo
489 489
 		//we need to get the DN from LDAP
490 490
 		$filter = $this->access->combineFilterWithAnd([
491 491
 			$this->access->connection->ldapGroupFilter,
492
-			'objectsid=' . $domainObjectSid . '-' . $gid
492
+			'objectsid='.$domainObjectSid.'-'.$gid
493 493
 		]);
494 494
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
495 495
 	}
@@ -546,7 +546,7 @@  discard block
 block discarded – undo
546 546
 		if ($search !== '') {
547 547
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
548 548
 		}
549
-		$filterParts[] = 'primaryGroupID=' . $groupID;
549
+		$filterParts[] = 'primaryGroupID='.$groupID;
550 550
 
551 551
 		return $this->access->combineFilterWithAnd($filterParts);
552 552
 	}
@@ -588,7 +588,7 @@  discard block
 block discarded – undo
588 588
 		try {
589 589
 			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
590 590
 			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
591
-			return (int)$users;
591
+			return (int) $users;
592 592
 		} catch (ServerNotAvailableException $e) {
593 593
 			throw $e;
594 594
 		} catch (Exception $e) {
@@ -627,7 +627,7 @@  discard block
 block discarded – undo
627 627
 		if (!$this->enabled) {
628 628
 			return [];
629 629
 		}
630
-		$cacheKey = 'getUserGroups' . $uid;
630
+		$cacheKey = 'getUserGroups'.$uid;
631 631
 		$userGroups = $this->access->connection->getFromCache($cacheKey);
632 632
 		if (!is_null($userGroups)) {
633 633
 			return $userGroups;
@@ -685,8 +685,8 @@  discard block
 block discarded – undo
685 685
 		// if possible, read out membership via memberOf. It's far faster than
686 686
 		// performing a search, which still is a fallback later.
687 687
 		// memberof doesn't support memberuid, so skip it here.
688
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
689
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
688
+		if ((int) $this->access->connection->hasMemberOfFilterSupport === 1
689
+			&& (int) $this->access->connection->useMemberOfToDetectMembership === 1
690 690
 			&& $this->ldapGroupMemberAssocAttr !== 'memberuid'
691 691
 			&& $this->ldapGroupMemberAssocAttr !== 'zimbramailforwardingaddress') {
692 692
 			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
@@ -778,7 +778,7 @@  discard block
 block discarded – undo
778 778
 		}
779 779
 		$allGroups = [];
780 780
 		$seen[$dn] = true;
781
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
781
+		$filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
782 782
 
783 783
 		if ($this->ldapGroupMemberAssocAttr === 'zimbramailforwardingaddress') {
784 784
 			//in this case the member entries are email addresses
@@ -788,7 +788,7 @@  discard block
 block discarded – undo
788 788
 		$groups = $this->access->fetchListOfGroups($filter,
789 789
 			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
790 790
 		if (is_array($groups)) {
791
-			$fetcher = function ($dn, &$seen) {
791
+			$fetcher = function($dn, &$seen) {
792 792
 				if (is_array($dn) && isset($dn['dn'][0])) {
793 793
 					$dn = $dn['dn'][0];
794 794
 				}
@@ -824,7 +824,7 @@  discard block
 block discarded – undo
824 824
 			return [];
825 825
 		}
826 826
 		$search = $this->access->escapeFilterPart($search, true);
827
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
827
+		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
828 828
 		// check for cache of the exact query
829 829
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
830 830
 		if (!is_null($groupUsers)) {
@@ -835,7 +835,7 @@  discard block
 block discarded – undo
835 835
 			$limit = null;
836 836
 		}
837 837
 		// check for cache of the query without limit and offset
838
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
838
+		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
839 839
 		if (!is_null($groupUsers)) {
840 840
 			$groupUsers = array_slice($groupUsers, $offset, $limit);
841 841
 			$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -889,7 +889,7 @@  discard block
 block discarded – undo
889 889
 						continue;
890 890
 					}
891 891
 
892
-					$cacheKey = 'userExistsOnLDAP' . $uid;
892
+					$cacheKey = 'userExistsOnLDAP'.$uid;
893 893
 					$userExists = $this->access->connection->getFromCache($cacheKey);
894 894
 					if ($userExists === false) {
895 895
 						continue;
@@ -915,7 +915,7 @@  discard block
 block discarded – undo
915 915
 
916 916
 		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
917 917
 		natsort($groupUsers);
918
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
918
+		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
919 919
 		$groupUsers = array_slice($groupUsers, $offset, $limit);
920 920
 
921 921
 		$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -937,7 +937,7 @@  discard block
 block discarded – undo
937 937
 			return $this->groupPluginManager->countUsersInGroup($gid, $search);
938 938
 		}
939 939
 
940
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
940
+		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
941 941
 		if (!$this->enabled || !$this->groupExists($gid)) {
942 942
 			return false;
943 943
 		}
@@ -1035,7 +1035,7 @@  discard block
 block discarded – undo
1035 1035
 		if (!$this->enabled) {
1036 1036
 			return [];
1037 1037
 		}
1038
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
1038
+		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
1039 1039
 
1040 1040
 		//Check cache before driving unnecessary searches
1041 1041
 		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
@@ -1070,31 +1070,31 @@  discard block
 block discarded – undo
1070 1070
 	 * @throws ServerNotAvailableException
1071 1071
 	 */
1072 1072
 	public function groupExists($gid) {
1073
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1073
+		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1074 1074
 		if (!is_null($groupExists)) {
1075
-			return (bool)$groupExists;
1075
+			return (bool) $groupExists;
1076 1076
 		}
1077 1077
 
1078 1078
 		//getting dn, if false the group does not exist. If dn, it may be mapped
1079 1079
 		//only, requires more checking.
1080 1080
 		$dn = $this->access->groupname2dn($gid);
1081 1081
 		if (!$dn) {
1082
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1082
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1083 1083
 			return false;
1084 1084
 		}
1085 1085
 
1086 1086
 		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1087
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1087
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1088 1088
 			return false;
1089 1089
 		}
1090 1090
 
1091 1091
 		//if group really still exists, we will be able to read its objectClass
1092 1092
 		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1093
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1093
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1094 1094
 			return false;
1095 1095
 		}
1096 1096
 
1097
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1097
+		$this->access->connection->writeToCache('groupExists'.$gid, true);
1098 1098
 		return true;
1099 1099
 	}
1100 1100
 
@@ -1127,7 +1127,7 @@  discard block
 block discarded – undo
1127 1127
 	 * compared with GroupInterface::CREATE_GROUP etc.
1128 1128
 	 */
1129 1129
 	public function implementsActions($actions) {
1130
-		return (bool)((GroupInterface::COUNT_USERS |
1130
+		return (bool) ((GroupInterface::COUNT_USERS |
1131 1131
 				$this->groupPluginManager->getImplementedActions()) & $actions);
1132 1132
 	}
1133 1133
 
@@ -1181,7 +1181,7 @@  discard block
 block discarded – undo
1181 1181
 			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1182 1182
 				#delete group in nextcloud internal db
1183 1183
 				$this->access->getGroupMapper()->unmap($gid);
1184
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1184
+				$this->access->connection->writeToCache("groupExists".$gid, false);
1185 1185
 			}
1186 1186
 			return $ret;
1187 1187
 		}
@@ -1262,7 +1262,7 @@  discard block
 block discarded – undo
1262 1262
 			return $this->groupPluginManager->getDisplayName($gid);
1263 1263
 		}
1264 1264
 
1265
-		$cacheKey = 'group_getDisplayName' . $gid;
1265
+		$cacheKey = 'group_getDisplayName'.$gid;
1266 1266
 		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1267 1267
 			return $displayName;
1268 1268
 		}
Please login to merge, or discard this patch.
apps/user_ldap/lib/Wizard.php 1 patch
Indentation   +1314 added lines, -1314 removed lines patch added patch discarded remove patch
@@ -42,1318 +42,1318 @@
 block discarded – undo
42 42
 use OCP\ILogger;
43 43
 
44 44
 class Wizard extends LDAPUtility {
45
-	/** @var \OCP\IL10N */
46
-	protected static $l;
47
-	protected $access;
48
-	protected $cr;
49
-	protected $configuration;
50
-	protected $result;
51
-	protected $resultCache = [];
52
-
53
-	public const LRESULT_PROCESSED_OK = 2;
54
-	public const LRESULT_PROCESSED_INVALID = 3;
55
-	public const LRESULT_PROCESSED_SKIP = 4;
56
-
57
-	public const LFILTER_LOGIN      = 2;
58
-	public const LFILTER_USER_LIST  = 3;
59
-	public const LFILTER_GROUP_LIST = 4;
60
-
61
-	public const LFILTER_MODE_ASSISTED = 2;
62
-	public const LFILTER_MODE_RAW = 1;
63
-
64
-	public const LDAP_NW_TIMEOUT = 4;
65
-
66
-	/**
67
-	 * Constructor
68
-	 * @param Configuration $configuration an instance of Configuration
69
-	 * @param ILDAPWrapper $ldap an instance of ILDAPWrapper
70
-	 * @param Access $access
71
-	 */
72
-	public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
73
-		parent::__construct($ldap);
74
-		$this->configuration = $configuration;
75
-		if (is_null(Wizard::$l)) {
76
-			Wizard::$l = \OC::$server->getL10N('user_ldap');
77
-		}
78
-		$this->access = $access;
79
-		$this->result = new WizardResult();
80
-	}
81
-
82
-	public function __destruct() {
83
-		if ($this->result->hasChanges()) {
84
-			$this->configuration->saveConfiguration();
85
-		}
86
-	}
87
-
88
-	/**
89
-	 * counts entries in the LDAP directory
90
-	 *
91
-	 * @param string $filter the LDAP search filter
92
-	 * @param string $type a string being either 'users' or 'groups';
93
-	 * @return int
94
-	 * @throws \Exception
95
-	 */
96
-	public function countEntries(string $filter, string $type): int {
97
-		$reqs = ['ldapHost', 'ldapPort', 'ldapBase'];
98
-		if ($type === 'users') {
99
-			$reqs[] = 'ldapUserFilter';
100
-		}
101
-		if (!$this->checkRequirements($reqs)) {
102
-			throw new \Exception('Requirements not met', 400);
103
-		}
104
-
105
-		$attr = ['dn']; // default
106
-		$limit = 1001;
107
-		if ($type === 'groups') {
108
-			$result =  $this->access->countGroups($filter, $attr, $limit);
109
-		} elseif ($type === 'users') {
110
-			$result = $this->access->countUsers($filter, $attr, $limit);
111
-		} elseif ($type === 'objects') {
112
-			$result = $this->access->countObjects($limit);
113
-		} else {
114
-			throw new \Exception('Internal error: Invalid object type', 500);
115
-		}
116
-
117
-		return (int)$result;
118
-	}
119
-
120
-	/**
121
-	 * formats the return value of a count operation to the string to be
122
-	 * inserted.
123
-	 *
124
-	 * @param int $count
125
-	 * @return string
126
-	 */
127
-	private function formatCountResult(int $count): string {
128
-		if ($count > 1000) {
129
-			return '> 1000';
130
-		}
131
-		return (string)$count;
132
-	}
133
-
134
-	public function countGroups() {
135
-		$filter = $this->configuration->ldapGroupFilter;
136
-
137
-		if (empty($filter)) {
138
-			$output = self::$l->n('%s group found', '%s groups found', 0, [0]);
139
-			$this->result->addChange('ldap_group_count', $output);
140
-			return $this->result;
141
-		}
142
-
143
-		try {
144
-			$groupsTotal = $this->countEntries($filter, 'groups');
145
-		} catch (\Exception $e) {
146
-			//400 can be ignored, 500 is forwarded
147
-			if ($e->getCode() === 500) {
148
-				throw $e;
149
-			}
150
-			return false;
151
-		}
152
-		$output = self::$l->n(
153
-			'%s group found',
154
-			'%s groups found',
155
-			$groupsTotal,
156
-			[$this->formatCountResult($groupsTotal)]
157
-		);
158
-		$this->result->addChange('ldap_group_count', $output);
159
-		return $this->result;
160
-	}
161
-
162
-	/**
163
-	 * @return WizardResult
164
-	 * @throws \Exception
165
-	 */
166
-	public function countUsers() {
167
-		$filter = $this->access->getFilterForUserCount();
168
-
169
-		$usersTotal = $this->countEntries($filter, 'users');
170
-		$output = self::$l->n(
171
-			'%s user found',
172
-			'%s users found',
173
-			$usersTotal,
174
-			[$this->formatCountResult($usersTotal)]
175
-		);
176
-		$this->result->addChange('ldap_user_count', $output);
177
-		return $this->result;
178
-	}
179
-
180
-	/**
181
-	 * counts any objects in the currently set base dn
182
-	 *
183
-	 * @return WizardResult
184
-	 * @throws \Exception
185
-	 */
186
-	public function countInBaseDN() {
187
-		// we don't need to provide a filter in this case
188
-		$total = $this->countEntries('', 'objects');
189
-		if ($total === false) {
190
-			throw new \Exception('invalid results received');
191
-		}
192
-		$this->result->addChange('ldap_test_base', $total);
193
-		return $this->result;
194
-	}
195
-
196
-	/**
197
-	 * counts users with a specified attribute
198
-	 * @param string $attr
199
-	 * @param bool $existsCheck
200
-	 * @return int|bool
201
-	 */
202
-	public function countUsersWithAttribute($attr, $existsCheck = false) {
203
-		if (!$this->checkRequirements(['ldapHost',
204
-			'ldapPort',
205
-			'ldapBase',
206
-			'ldapUserFilter',
207
-		])) {
208
-			return  false;
209
-		}
210
-
211
-		$filter = $this->access->combineFilterWithAnd([
212
-			$this->configuration->ldapUserFilter,
213
-			$attr . '=*'
214
-		]);
215
-
216
-		$limit = ($existsCheck === false) ? null : 1;
217
-
218
-		return $this->access->countUsers($filter, ['dn'], $limit);
219
-	}
220
-
221
-	/**
222
-	 * detects the display name attribute. If a setting is already present that
223
-	 * returns at least one hit, the detection will be canceled.
224
-	 * @return WizardResult|bool
225
-	 * @throws \Exception
226
-	 */
227
-	public function detectUserDisplayNameAttribute() {
228
-		if (!$this->checkRequirements(['ldapHost',
229
-			'ldapPort',
230
-			'ldapBase',
231
-			'ldapUserFilter',
232
-		])) {
233
-			return  false;
234
-		}
235
-
236
-		$attr = $this->configuration->ldapUserDisplayName;
237
-		if ($attr !== '' && $attr !== 'displayName') {
238
-			// most likely not the default value with upper case N,
239
-			// verify it still produces a result
240
-			$count = (int)$this->countUsersWithAttribute($attr, true);
241
-			if ($count > 0) {
242
-				//no change, but we sent it back to make sure the user interface
243
-				//is still correct, even if the ajax call was cancelled meanwhile
244
-				$this->result->addChange('ldap_display_name', $attr);
245
-				return $this->result;
246
-			}
247
-		}
248
-
249
-		// first attribute that has at least one result wins
250
-		$displayNameAttrs = ['displayname', 'cn'];
251
-		foreach ($displayNameAttrs as $attr) {
252
-			$count = (int)$this->countUsersWithAttribute($attr, true);
253
-
254
-			if ($count > 0) {
255
-				$this->applyFind('ldap_display_name', $attr);
256
-				return $this->result;
257
-			}
258
-		}
259
-
260
-		throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.'));
261
-	}
262
-
263
-	/**
264
-	 * detects the most often used email attribute for users applying to the
265
-	 * user list filter. If a setting is already present that returns at least
266
-	 * one hit, the detection will be canceled.
267
-	 * @return WizardResult|bool
268
-	 */
269
-	public function detectEmailAttribute() {
270
-		if (!$this->checkRequirements(['ldapHost',
271
-			'ldapPort',
272
-			'ldapBase',
273
-			'ldapUserFilter',
274
-		])) {
275
-			return  false;
276
-		}
277
-
278
-		$attr = $this->configuration->ldapEmailAttribute;
279
-		if ($attr !== '') {
280
-			$count = (int)$this->countUsersWithAttribute($attr, true);
281
-			if ($count > 0) {
282
-				return false;
283
-			}
284
-			$writeLog = true;
285
-		} else {
286
-			$writeLog = false;
287
-		}
288
-
289
-		$emailAttributes = ['mail', 'mailPrimaryAddress'];
290
-		$winner = '';
291
-		$maxUsers = 0;
292
-		foreach ($emailAttributes as $attr) {
293
-			$count = $this->countUsersWithAttribute($attr);
294
-			if ($count > $maxUsers) {
295
-				$maxUsers = $count;
296
-				$winner = $attr;
297
-			}
298
-		}
299
-
300
-		if ($winner !== '') {
301
-			$this->applyFind('ldap_email_attr', $winner);
302
-			if ($writeLog) {
303
-				\OCP\Util::writeLog('user_ldap', 'The mail attribute has ' .
304
-					'automatically been reset, because the original value ' .
305
-					'did not return any results.', ILogger::INFO);
306
-			}
307
-		}
308
-
309
-		return $this->result;
310
-	}
311
-
312
-	/**
313
-	 * @return WizardResult
314
-	 * @throws \Exception
315
-	 */
316
-	public function determineAttributes() {
317
-		if (!$this->checkRequirements(['ldapHost',
318
-			'ldapPort',
319
-			'ldapBase',
320
-			'ldapUserFilter',
321
-		])) {
322
-			return  false;
323
-		}
324
-
325
-		$attributes = $this->getUserAttributes();
326
-
327
-		natcasesort($attributes);
328
-		$attributes = array_values($attributes);
329
-
330
-		$this->result->addOptions('ldap_loginfilter_attributes', $attributes);
331
-
332
-		$selected = $this->configuration->ldapLoginFilterAttributes;
333
-		if (is_array($selected) && !empty($selected)) {
334
-			$this->result->addChange('ldap_loginfilter_attributes', $selected);
335
-		}
336
-
337
-		return $this->result;
338
-	}
339
-
340
-	/**
341
-	 * detects the available LDAP attributes
342
-	 * @return array|false The instance's WizardResult instance
343
-	 * @throws \Exception
344
-	 */
345
-	private function getUserAttributes() {
346
-		if (!$this->checkRequirements(['ldapHost',
347
-			'ldapPort',
348
-			'ldapBase',
349
-			'ldapUserFilter',
350
-		])) {
351
-			return  false;
352
-		}
353
-		$cr = $this->getConnection();
354
-		if (!$cr) {
355
-			throw new \Exception('Could not connect to LDAP');
356
-		}
357
-
358
-		$base = $this->configuration->ldapBase[0];
359
-		$filter = $this->configuration->ldapUserFilter;
360
-		$rr = $this->ldap->search($cr, $base, $filter, [], 1, 1);
361
-		if (!$this->ldap->isResource($rr)) {
362
-			return false;
363
-		}
364
-		$er = $this->ldap->firstEntry($cr, $rr);
365
-		$attributes = $this->ldap->getAttributes($cr, $er);
366
-		$pureAttributes = [];
367
-		for ($i = 0; $i < $attributes['count']; $i++) {
368
-			$pureAttributes[] = $attributes[$i];
369
-		}
370
-
371
-		return $pureAttributes;
372
-	}
373
-
374
-	/**
375
-	 * detects the available LDAP groups
376
-	 * @return WizardResult|false the instance's WizardResult instance
377
-	 */
378
-	public function determineGroupsForGroups() {
379
-		return $this->determineGroups('ldap_groupfilter_groups',
380
-									  'ldapGroupFilterGroups',
381
-									  false);
382
-	}
383
-
384
-	/**
385
-	 * detects the available LDAP groups
386
-	 * @return WizardResult|false the instance's WizardResult instance
387
-	 */
388
-	public function determineGroupsForUsers() {
389
-		return $this->determineGroups('ldap_userfilter_groups',
390
-									  'ldapUserFilterGroups');
391
-	}
392
-
393
-	/**
394
-	 * detects the available LDAP groups
395
-	 * @param string $dbKey
396
-	 * @param string $confKey
397
-	 * @param bool $testMemberOf
398
-	 * @return WizardResult|false the instance's WizardResult instance
399
-	 * @throws \Exception
400
-	 */
401
-	private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
402
-		if (!$this->checkRequirements(['ldapHost',
403
-			'ldapPort',
404
-			'ldapBase',
405
-		])) {
406
-			return  false;
407
-		}
408
-		$cr = $this->getConnection();
409
-		if (!$cr) {
410
-			throw new \Exception('Could not connect to LDAP');
411
-		}
412
-
413
-		$this->fetchGroups($dbKey, $confKey);
414
-
415
-		if ($testMemberOf) {
416
-			$this->configuration->hasMemberOfFilterSupport = $this->testMemberOf();
417
-			$this->result->markChange();
418
-			if (!$this->configuration->hasMemberOfFilterSupport) {
419
-				throw new \Exception('memberOf is not supported by the server');
420
-			}
421
-		}
422
-
423
-		return $this->result;
424
-	}
425
-
426
-	/**
427
-	 * fetches all groups from LDAP and adds them to the result object
428
-	 *
429
-	 * @param string $dbKey
430
-	 * @param string $confKey
431
-	 * @return array $groupEntries
432
-	 * @throws \Exception
433
-	 */
434
-	public function fetchGroups($dbKey, $confKey) {
435
-		$obclasses = ['posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames'];
436
-
437
-		$filterParts = [];
438
-		foreach ($obclasses as $obclass) {
439
-			$filterParts[] = 'objectclass='.$obclass;
440
-		}
441
-		//we filter for everything
442
-		//- that looks like a group and
443
-		//- has the group display name set
444
-		$filter = $this->access->combineFilterWithOr($filterParts);
445
-		$filter = $this->access->combineFilterWithAnd([$filter, 'cn=*']);
446
-
447
-		$groupNames = [];
448
-		$groupEntries = [];
449
-		$limit = 400;
450
-		$offset = 0;
451
-		do {
452
-			// we need to request dn additionally here, otherwise memberOf
453
-			// detection will fail later
454
-			$result = $this->access->searchGroups($filter, ['cn', 'dn'], $limit, $offset);
455
-			foreach ($result as $item) {
456
-				if (!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) {
457
-					// just in case - no issue known
458
-					continue;
459
-				}
460
-				$groupNames[] = $item['cn'][0];
461
-				$groupEntries[] = $item;
462
-			}
463
-			$offset += $limit;
464
-		} while ($this->access->hasMoreResults());
465
-
466
-		if (count($groupNames) > 0) {
467
-			natsort($groupNames);
468
-			$this->result->addOptions($dbKey, array_values($groupNames));
469
-		} else {
470
-			throw new \Exception(self::$l->t('Could not find the desired feature'));
471
-		}
472
-
473
-		$setFeatures = $this->configuration->$confKey;
474
-		if (is_array($setFeatures) && !empty($setFeatures)) {
475
-			//something is already configured? pre-select it.
476
-			$this->result->addChange($dbKey, $setFeatures);
477
-		}
478
-		return $groupEntries;
479
-	}
480
-
481
-	public function determineGroupMemberAssoc() {
482
-		if (!$this->checkRequirements(['ldapHost',
483
-			'ldapPort',
484
-			'ldapGroupFilter',
485
-		])) {
486
-			return  false;
487
-		}
488
-		$attribute = $this->detectGroupMemberAssoc();
489
-		if ($attribute === false) {
490
-			return false;
491
-		}
492
-		$this->configuration->setConfiguration(['ldapGroupMemberAssocAttr' => $attribute]);
493
-		$this->result->addChange('ldap_group_member_assoc_attribute', $attribute);
494
-
495
-		return $this->result;
496
-	}
497
-
498
-	/**
499
-	 * Detects the available object classes
500
-	 * @return WizardResult|false the instance's WizardResult instance
501
-	 * @throws \Exception
502
-	 */
503
-	public function determineGroupObjectClasses() {
504
-		if (!$this->checkRequirements(['ldapHost',
505
-			'ldapPort',
506
-			'ldapBase',
507
-		])) {
508
-			return  false;
509
-		}
510
-		$cr = $this->getConnection();
511
-		if (!$cr) {
512
-			throw new \Exception('Could not connect to LDAP');
513
-		}
514
-
515
-		$obclasses = ['groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*'];
516
-		$this->determineFeature($obclasses,
517
-								'objectclass',
518
-								'ldap_groupfilter_objectclass',
519
-								'ldapGroupFilterObjectclass',
520
-								false);
521
-
522
-		return $this->result;
523
-	}
524
-
525
-	/**
526
-	 * detects the available object classes
527
-	 * @return WizardResult
528
-	 * @throws \Exception
529
-	 */
530
-	public function determineUserObjectClasses() {
531
-		if (!$this->checkRequirements(['ldapHost',
532
-			'ldapPort',
533
-			'ldapBase',
534
-		])) {
535
-			return  false;
536
-		}
537
-		$cr = $this->getConnection();
538
-		if (!$cr) {
539
-			throw new \Exception('Could not connect to LDAP');
540
-		}
541
-
542
-		$obclasses = ['inetOrgPerson', 'person', 'organizationalPerson',
543
-			'user', 'posixAccount', '*'];
544
-		$filter = $this->configuration->ldapUserFilter;
545
-		//if filter is empty, it is probably the first time the wizard is called
546
-		//then, apply suggestions.
547
-		$this->determineFeature($obclasses,
548
-								'objectclass',
549
-								'ldap_userfilter_objectclass',
550
-								'ldapUserFilterObjectclass',
551
-								empty($filter));
552
-
553
-		return $this->result;
554
-	}
555
-
556
-	/**
557
-	 * @return WizardResult|false
558
-	 * @throws \Exception
559
-	 */
560
-	public function getGroupFilter() {
561
-		if (!$this->checkRequirements(['ldapHost',
562
-			'ldapPort',
563
-			'ldapBase',
564
-		])) {
565
-			return false;
566
-		}
567
-		//make sure the use display name is set
568
-		$displayName = $this->configuration->ldapGroupDisplayName;
569
-		if ($displayName === '') {
570
-			$d = $this->configuration->getDefaults();
571
-			$this->applyFind('ldap_group_display_name',
572
-							 $d['ldap_group_display_name']);
573
-		}
574
-		$filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
575
-
576
-		$this->applyFind('ldap_group_filter', $filter);
577
-		return $this->result;
578
-	}
579
-
580
-	/**
581
-	 * @return WizardResult|false
582
-	 * @throws \Exception
583
-	 */
584
-	public function getUserListFilter() {
585
-		if (!$this->checkRequirements(['ldapHost',
586
-			'ldapPort',
587
-			'ldapBase',
588
-		])) {
589
-			return false;
590
-		}
591
-		//make sure the use display name is set
592
-		$displayName = $this->configuration->ldapUserDisplayName;
593
-		if ($displayName === '') {
594
-			$d = $this->configuration->getDefaults();
595
-			$this->applyFind('ldap_display_name', $d['ldap_display_name']);
596
-		}
597
-		$filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
598
-		if (!$filter) {
599
-			throw new \Exception('Cannot create filter');
600
-		}
601
-
602
-		$this->applyFind('ldap_userlist_filter', $filter);
603
-		return $this->result;
604
-	}
605
-
606
-	/**
607
-	 * @return bool|WizardResult
608
-	 * @throws \Exception
609
-	 */
610
-	public function getUserLoginFilter() {
611
-		if (!$this->checkRequirements(['ldapHost',
612
-			'ldapPort',
613
-			'ldapBase',
614
-			'ldapUserFilter',
615
-		])) {
616
-			return false;
617
-		}
618
-
619
-		$filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
620
-		if (!$filter) {
621
-			throw new \Exception('Cannot create filter');
622
-		}
623
-
624
-		$this->applyFind('ldap_login_filter', $filter);
625
-		return $this->result;
626
-	}
627
-
628
-	/**
629
-	 * @return bool|WizardResult
630
-	 * @param string $loginName
631
-	 * @throws \Exception
632
-	 */
633
-	public function testLoginName($loginName) {
634
-		if (!$this->checkRequirements(['ldapHost',
635
-			'ldapPort',
636
-			'ldapBase',
637
-			'ldapLoginFilter',
638
-		])) {
639
-			return false;
640
-		}
641
-
642
-		$cr = $this->access->connection->getConnectionResource();
643
-		if (!$this->ldap->isResource($cr)) {
644
-			throw new \Exception('connection error');
645
-		}
646
-
647
-		if (mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8')
648
-			=== false) {
649
-			throw new \Exception('missing placeholder');
650
-		}
651
-
652
-		$users = $this->access->countUsersByLoginName($loginName);
653
-		if ($this->ldap->errno($cr) !== 0) {
654
-			throw new \Exception($this->ldap->error($cr));
655
-		}
656
-		$filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter);
657
-		$this->result->addChange('ldap_test_loginname', $users);
658
-		$this->result->addChange('ldap_test_effective_filter', $filter);
659
-		return $this->result;
660
-	}
661
-
662
-	/**
663
-	 * Tries to determine the port, requires given Host, User DN and Password
664
-	 * @return WizardResult|false WizardResult on success, false otherwise
665
-	 * @throws \Exception
666
-	 */
667
-	public function guessPortAndTLS() {
668
-		if (!$this->checkRequirements(['ldapHost',
669
-		])) {
670
-			return false;
671
-		}
672
-		$this->checkHost();
673
-		$portSettings = $this->getPortSettingsToTry();
674
-
675
-		if (!is_array($portSettings)) {
676
-			throw new \Exception(print_r($portSettings, true));
677
-		}
678
-
679
-		//proceed from the best configuration and return on first success
680
-		foreach ($portSettings as $setting) {
681
-			$p = $setting['port'];
682
-			$t = $setting['tls'];
683
-			\OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, ILogger::DEBUG);
684
-			//connectAndBind may throw Exception, it needs to be catched by the
685
-			//callee of this method
686
-
687
-			try {
688
-				$settingsFound = $this->connectAndBind($p, $t);
689
-			} catch (\Exception $e) {
690
-				// any reply other than -1 (= cannot connect) is already okay,
691
-				// because then we found the server
692
-				// unavailable startTLS returns -11
693
-				if ($e->getCode() > 0) {
694
-					$settingsFound = true;
695
-				} else {
696
-					throw $e;
697
-				}
698
-			}
699
-
700
-			if ($settingsFound === true) {
701
-				$config = [
702
-					'ldapPort' => $p,
703
-					'ldapTLS' => (int)$t
704
-				];
705
-				$this->configuration->setConfiguration($config);
706
-				\OCP\Util::writeLog('user_ldap', 'Wiz: detected Port ' . $p, ILogger::DEBUG);
707
-				$this->result->addChange('ldap_port', $p);
708
-				return $this->result;
709
-			}
710
-		}
711
-
712
-		//custom port, undetected (we do not brute force)
713
-		return false;
714
-	}
715
-
716
-	/**
717
-	 * tries to determine a base dn from User DN or LDAP Host
718
-	 * @return WizardResult|false WizardResult on success, false otherwise
719
-	 */
720
-	public function guessBaseDN() {
721
-		if (!$this->checkRequirements(['ldapHost',
722
-			'ldapPort',
723
-		])) {
724
-			return false;
725
-		}
726
-
727
-		//check whether a DN is given in the agent name (99.9% of all cases)
728
-		$base = null;
729
-		$i = stripos($this->configuration->ldapAgentName, 'dc=');
730
-		if ($i !== false) {
731
-			$base = substr($this->configuration->ldapAgentName, $i);
732
-			if ($this->testBaseDN($base)) {
733
-				$this->applyFind('ldap_base', $base);
734
-				return $this->result;
735
-			}
736
-		}
737
-
738
-		//this did not help :(
739
-		//Let's see whether we can parse the Host URL and convert the domain to
740
-		//a base DN
741
-		$helper = new Helper(\OC::$server->getConfig());
742
-		$domain = $helper->getDomainFromURL($this->configuration->ldapHost);
743
-		if (!$domain) {
744
-			return false;
745
-		}
746
-
747
-		$dparts = explode('.', $domain);
748
-		while (count($dparts) > 0) {
749
-			$base2 = 'dc=' . implode(',dc=', $dparts);
750
-			if ($base !== $base2 && $this->testBaseDN($base2)) {
751
-				$this->applyFind('ldap_base', $base2);
752
-				return $this->result;
753
-			}
754
-			array_shift($dparts);
755
-		}
756
-
757
-		return false;
758
-	}
759
-
760
-	/**
761
-	 * sets the found value for the configuration key in the WizardResult
762
-	 * as well as in the Configuration instance
763
-	 * @param string $key the configuration key
764
-	 * @param string $value the (detected) value
765
-	 *
766
-	 */
767
-	private function applyFind($key, $value) {
768
-		$this->result->addChange($key, $value);
769
-		$this->configuration->setConfiguration([$key => $value]);
770
-	}
771
-
772
-	/**
773
-	 * Checks, whether a port was entered in the Host configuration
774
-	 * field. In this case the port will be stripped off, but also stored as
775
-	 * setting.
776
-	 */
777
-	private function checkHost() {
778
-		$host = $this->configuration->ldapHost;
779
-		$hostInfo = parse_url($host);
780
-
781
-		//removes Port from Host
782
-		if (is_array($hostInfo) && isset($hostInfo['port'])) {
783
-			$port = $hostInfo['port'];
784
-			$host = str_replace(':'.$port, '', $host);
785
-			$this->applyFind('ldap_host', $host);
786
-			$this->applyFind('ldap_port', $port);
787
-		}
788
-	}
789
-
790
-	/**
791
-	 * tries to detect the group member association attribute which is
792
-	 * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
793
-	 * @return string|false, string with the attribute name, false on error
794
-	 * @throws \Exception
795
-	 */
796
-	private function detectGroupMemberAssoc() {
797
-		$possibleAttrs = ['uniqueMember', 'memberUid', 'member', 'gidNumber', 'zimbraMailForwardingAddress'];
798
-		$filter = $this->configuration->ldapGroupFilter;
799
-		if (empty($filter)) {
800
-			return false;
801
-		}
802
-		$cr = $this->getConnection();
803
-		if (!$cr) {
804
-			throw new \Exception('Could not connect to LDAP');
805
-		}
806
-		$base = $this->configuration->ldapBaseGroups[0] ?: $this->configuration->ldapBase[0];
807
-		$rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000);
808
-		if (!$this->ldap->isResource($rr)) {
809
-			return false;
810
-		}
811
-		$er = $this->ldap->firstEntry($cr, $rr);
812
-		while (is_resource($er)) {
813
-			$this->ldap->getDN($cr, $er);
814
-			$attrs = $this->ldap->getAttributes($cr, $er);
815
-			$result = [];
816
-			$possibleAttrsCount = count($possibleAttrs);
817
-			for ($i = 0; $i < $possibleAttrsCount; $i++) {
818
-				if (isset($attrs[$possibleAttrs[$i]])) {
819
-					$result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
820
-				}
821
-			}
822
-			if (!empty($result)) {
823
-				natsort($result);
824
-				return key($result);
825
-			}
826
-
827
-			$er = $this->ldap->nextEntry($cr, $er);
828
-		}
829
-
830
-		return false;
831
-	}
832
-
833
-	/**
834
-	 * Checks whether for a given BaseDN results will be returned
835
-	 * @param string $base the BaseDN to test
836
-	 * @return bool true on success, false otherwise
837
-	 * @throws \Exception
838
-	 */
839
-	private function testBaseDN($base) {
840
-		$cr = $this->getConnection();
841
-		if (!$cr) {
842
-			throw new \Exception('Could not connect to LDAP');
843
-		}
844
-
845
-		//base is there, let's validate it. If we search for anything, we should
846
-		//get a result set > 0 on a proper base
847
-		$rr = $this->ldap->search($cr, $base, 'objectClass=*', ['dn'], 0, 1);
848
-		if (!$this->ldap->isResource($rr)) {
849
-			$errorNo  = $this->ldap->errno($cr);
850
-			$errorMsg = $this->ldap->error($cr);
851
-			\OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base.
852
-							' Error '.$errorNo.': '.$errorMsg, ILogger::INFO);
853
-			return false;
854
-		}
855
-		$entries = $this->ldap->countEntries($cr, $rr);
856
-		return ($entries !== false) && ($entries > 0);
857
-	}
858
-
859
-	/**
860
-	 * Checks whether the server supports memberOf in LDAP Filter.
861
-	 * Note: at least in OpenLDAP, availability of memberOf is dependent on
862
-	 * a configured objectClass. I.e. not necessarily for all available groups
863
-	 * memberOf does work.
864
-	 *
865
-	 * @return bool true if it does, false otherwise
866
-	 * @throws \Exception
867
-	 */
868
-	private function testMemberOf() {
869
-		$cr = $this->getConnection();
870
-		if (!$cr) {
871
-			throw new \Exception('Could not connect to LDAP');
872
-		}
873
-		$result = $this->access->countUsers('memberOf=*', ['memberOf'], 1);
874
-		if (is_int($result) &&  $result > 0) {
875
-			return true;
876
-		}
877
-		return false;
878
-	}
879
-
880
-	/**
881
-	 * creates an LDAP Filter from given configuration
882
-	 * @param integer $filterType int, for which use case the filter shall be created
883
-	 * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
884
-	 * self::LFILTER_GROUP_LIST
885
-	 * @return string|false string with the filter on success, false otherwise
886
-	 * @throws \Exception
887
-	 */
888
-	private function composeLdapFilter($filterType) {
889
-		$filter = '';
890
-		$parts = 0;
891
-		switch ($filterType) {
892
-			case self::LFILTER_USER_LIST:
893
-				$objcs = $this->configuration->ldapUserFilterObjectclass;
894
-				//glue objectclasses
895
-				if (is_array($objcs) && count($objcs) > 0) {
896
-					$filter .= '(|';
897
-					foreach ($objcs as $objc) {
898
-						$filter .= '(objectclass=' . $objc . ')';
899
-					}
900
-					$filter .= ')';
901
-					$parts++;
902
-				}
903
-				//glue group memberships
904
-				if ($this->configuration->hasMemberOfFilterSupport) {
905
-					$cns = $this->configuration->ldapUserFilterGroups;
906
-					if (is_array($cns) && count($cns) > 0) {
907
-						$filter .= '(|';
908
-						$cr = $this->getConnection();
909
-						if (!$cr) {
910
-							throw new \Exception('Could not connect to LDAP');
911
-						}
912
-						$base = $this->configuration->ldapBase[0];
913
-						foreach ($cns as $cn) {
914
-							$rr = $this->ldap->search($cr, $base, 'cn=' . $cn, ['dn', 'primaryGroupToken']);
915
-							if (!$this->ldap->isResource($rr)) {
916
-								continue;
917
-							}
918
-							$er = $this->ldap->firstEntry($cr, $rr);
919
-							$attrs = $this->ldap->getAttributes($cr, $er);
920
-							$dn = $this->ldap->getDN($cr, $er);
921
-							if ($dn === false || $dn === '') {
922
-								continue;
923
-							}
924
-							$filterPart = '(memberof=' . $dn . ')';
925
-							if (isset($attrs['primaryGroupToken'])) {
926
-								$pgt = $attrs['primaryGroupToken'][0];
927
-								$primaryFilterPart = '(primaryGroupID=' . $pgt .')';
928
-								$filterPart = '(|' . $filterPart . $primaryFilterPart . ')';
929
-							}
930
-							$filter .= $filterPart;
931
-						}
932
-						$filter .= ')';
933
-					}
934
-					$parts++;
935
-				}
936
-				//wrap parts in AND condition
937
-				if ($parts > 1) {
938
-					$filter = '(&' . $filter . ')';
939
-				}
940
-				if ($filter === '') {
941
-					$filter = '(objectclass=*)';
942
-				}
943
-				break;
944
-
945
-			case self::LFILTER_GROUP_LIST:
946
-				$objcs = $this->configuration->ldapGroupFilterObjectclass;
947
-				//glue objectclasses
948
-				if (is_array($objcs) && count($objcs) > 0) {
949
-					$filter .= '(|';
950
-					foreach ($objcs as $objc) {
951
-						$filter .= '(objectclass=' . $objc . ')';
952
-					}
953
-					$filter .= ')';
954
-					$parts++;
955
-				}
956
-				//glue group memberships
957
-				$cns = $this->configuration->ldapGroupFilterGroups;
958
-				if (is_array($cns) && count($cns) > 0) {
959
-					$filter .= '(|';
960
-					foreach ($cns as $cn) {
961
-						$filter .= '(cn=' . $cn . ')';
962
-					}
963
-					$filter .= ')';
964
-				}
965
-				$parts++;
966
-				//wrap parts in AND condition
967
-				if ($parts > 1) {
968
-					$filter = '(&' . $filter . ')';
969
-				}
970
-				break;
971
-
972
-			case self::LFILTER_LOGIN:
973
-				$ulf = $this->configuration->ldapUserFilter;
974
-				$loginpart = '=%uid';
975
-				$filterUsername = '';
976
-				$userAttributes = $this->getUserAttributes();
977
-				$userAttributes = array_change_key_case(array_flip($userAttributes));
978
-				$parts = 0;
979
-
980
-				if ($this->configuration->ldapLoginFilterUsername === '1') {
981
-					$attr = '';
982
-					if (isset($userAttributes['uid'])) {
983
-						$attr = 'uid';
984
-					} elseif (isset($userAttributes['samaccountname'])) {
985
-						$attr = 'samaccountname';
986
-					} elseif (isset($userAttributes['cn'])) {
987
-						//fallback
988
-						$attr = 'cn';
989
-					}
990
-					if ($attr !== '') {
991
-						$filterUsername = '(' . $attr . $loginpart . ')';
992
-						$parts++;
993
-					}
994
-				}
995
-
996
-				$filterEmail = '';
997
-				if ($this->configuration->ldapLoginFilterEmail === '1') {
998
-					$filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
999
-					$parts++;
1000
-				}
1001
-
1002
-				$filterAttributes = '';
1003
-				$attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
1004
-				if (is_array($attrsToFilter) && count($attrsToFilter) > 0) {
1005
-					$filterAttributes = '(|';
1006
-					foreach ($attrsToFilter as $attribute) {
1007
-						$filterAttributes .= '(' . $attribute . $loginpart . ')';
1008
-					}
1009
-					$filterAttributes .= ')';
1010
-					$parts++;
1011
-				}
1012
-
1013
-				$filterLogin = '';
1014
-				if ($parts > 1) {
1015
-					$filterLogin = '(|';
1016
-				}
1017
-				$filterLogin .= $filterUsername;
1018
-				$filterLogin .= $filterEmail;
1019
-				$filterLogin .= $filterAttributes;
1020
-				if ($parts > 1) {
1021
-					$filterLogin .= ')';
1022
-				}
1023
-
1024
-				$filter = '(&'.$ulf.$filterLogin.')';
1025
-				break;
1026
-		}
1027
-
1028
-		\OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, ILogger::DEBUG);
1029
-
1030
-		return $filter;
1031
-	}
1032
-
1033
-	/**
1034
-	 * Connects and Binds to an LDAP Server
1035
-	 *
1036
-	 * @param int $port the port to connect with
1037
-	 * @param bool $tls whether startTLS is to be used
1038
-	 * @return bool
1039
-	 * @throws \Exception
1040
-	 */
1041
-	private function connectAndBind($port, $tls) {
1042
-		//connect, does not really trigger any server communication
1043
-		$host = $this->configuration->ldapHost;
1044
-		$hostInfo = parse_url($host);
1045
-		if (!$hostInfo) {
1046
-			throw new \Exception(self::$l->t('Invalid Host'));
1047
-		}
1048
-		\OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', ILogger::DEBUG);
1049
-		$cr = $this->ldap->connect($host, $port);
1050
-		if (!is_resource($cr)) {
1051
-			throw new \Exception(self::$l->t('Invalid Host'));
1052
-		}
1053
-
1054
-		//set LDAP options
1055
-		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1056
-		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1057
-		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1058
-
1059
-		try {
1060
-			if ($tls) {
1061
-				$isTlsWorking = @$this->ldap->startTls($cr);
1062
-				if (!$isTlsWorking) {
1063
-					return false;
1064
-				}
1065
-			}
1066
-
1067
-			\OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', ILogger::DEBUG);
1068
-			//interesting part: do the bind!
1069
-			$login = $this->ldap->bind($cr,
1070
-				$this->configuration->ldapAgentName,
1071
-				$this->configuration->ldapAgentPassword
1072
-			);
1073
-			$errNo = $this->ldap->errno($cr);
1074
-			$error = ldap_error($cr);
1075
-			$this->ldap->unbind($cr);
1076
-		} catch (ServerNotAvailableException $e) {
1077
-			return false;
1078
-		}
1079
-
1080
-		if ($login === true) {
1081
-			$this->ldap->unbind($cr);
1082
-			\OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, ILogger::DEBUG);
1083
-			return true;
1084
-		}
1085
-
1086
-		if ($errNo === -1) {
1087
-			//host, port or TLS wrong
1088
-			return false;
1089
-		}
1090
-		throw new \Exception($error, $errNo);
1091
-	}
1092
-
1093
-	/**
1094
-	 * checks whether a valid combination of agent and password has been
1095
-	 * provided (either two values or nothing for anonymous connect)
1096
-	 * @return bool, true if everything is fine, false otherwise
1097
-	 */
1098
-	private function checkAgentRequirements() {
1099
-		$agent = $this->configuration->ldapAgentName;
1100
-		$pwd = $this->configuration->ldapAgentPassword;
1101
-
1102
-		return
1103
-			($agent !== '' && $pwd !== '')
1104
-			||  ($agent === '' && $pwd === '')
1105
-		;
1106
-	}
1107
-
1108
-	/**
1109
-	 * @param array $reqs
1110
-	 * @return bool
1111
-	 */
1112
-	private function checkRequirements($reqs) {
1113
-		$this->checkAgentRequirements();
1114
-		foreach ($reqs as $option) {
1115
-			$value = $this->configuration->$option;
1116
-			if (empty($value)) {
1117
-				return false;
1118
-			}
1119
-		}
1120
-		return true;
1121
-	}
1122
-
1123
-	/**
1124
-	 * does a cumulativeSearch on LDAP to get different values of a
1125
-	 * specified attribute
1126
-	 * @param string[] $filters array, the filters that shall be used in the search
1127
-	 * @param string $attr the attribute of which a list of values shall be returned
1128
-	 * @param int $dnReadLimit the amount of how many DNs should be analyzed.
1129
-	 * The lower, the faster
1130
-	 * @param string $maxF string. if not null, this variable will have the filter that
1131
-	 * yields most result entries
1132
-	 * @return array|false an array with the values on success, false otherwise
1133
-	 */
1134
-	public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
1135
-		$dnRead = [];
1136
-		$foundItems = [];
1137
-		$maxEntries = 0;
1138
-		if (!is_array($this->configuration->ldapBase)
1139
-		   || !isset($this->configuration->ldapBase[0])) {
1140
-			return false;
1141
-		}
1142
-		$base = $this->configuration->ldapBase[0];
1143
-		$cr = $this->getConnection();
1144
-		if (!$this->ldap->isResource($cr)) {
1145
-			return false;
1146
-		}
1147
-		$lastFilter = null;
1148
-		if (isset($filters[count($filters)-1])) {
1149
-			$lastFilter = $filters[count($filters)-1];
1150
-		}
1151
-		foreach ($filters as $filter) {
1152
-			if ($lastFilter === $filter && count($foundItems) > 0) {
1153
-				//skip when the filter is a wildcard and results were found
1154
-				continue;
1155
-			}
1156
-			// 20k limit for performance and reason
1157
-			$rr = $this->ldap->search($cr, $base, $filter, [$attr], 0, 20000);
1158
-			if (!$this->ldap->isResource($rr)) {
1159
-				continue;
1160
-			}
1161
-			$entries = $this->ldap->countEntries($cr, $rr);
1162
-			$getEntryFunc = 'firstEntry';
1163
-			if (($entries !== false) && ($entries > 0)) {
1164
-				if (!is_null($maxF) && $entries > $maxEntries) {
1165
-					$maxEntries = $entries;
1166
-					$maxF = $filter;
1167
-				}
1168
-				$dnReadCount = 0;
1169
-				do {
1170
-					$entry = $this->ldap->$getEntryFunc($cr, $rr);
1171
-					$getEntryFunc = 'nextEntry';
1172
-					if (!$this->ldap->isResource($entry)) {
1173
-						continue 2;
1174
-					}
1175
-					$rr = $entry; //will be expected by nextEntry next round
1176
-					$attributes = $this->ldap->getAttributes($cr, $entry);
1177
-					$dn = $this->ldap->getDN($cr, $entry);
1178
-					if ($dn === false || in_array($dn, $dnRead)) {
1179
-						continue;
1180
-					}
1181
-					$newItems = [];
1182
-					$state = $this->getAttributeValuesFromEntry($attributes,
1183
-																$attr,
1184
-																$newItems);
1185
-					$dnReadCount++;
1186
-					$foundItems = array_merge($foundItems, $newItems);
1187
-					$this->resultCache[$dn][$attr] = $newItems;
1188
-					$dnRead[] = $dn;
1189
-				} while (($state === self::LRESULT_PROCESSED_SKIP
1190
-						|| $this->ldap->isResource($entry))
1191
-						&& ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
1192
-			}
1193
-		}
1194
-
1195
-		return array_unique($foundItems);
1196
-	}
1197
-
1198
-	/**
1199
-	 * determines if and which $attr are available on the LDAP server
1200
-	 * @param string[] $objectclasses the objectclasses to use as search filter
1201
-	 * @param string $attr the attribute to look for
1202
-	 * @param string $dbkey the dbkey of the setting the feature is connected to
1203
-	 * @param string $confkey the confkey counterpart for the $dbkey as used in the
1204
-	 * Configuration class
1205
-	 * @param bool $po whether the objectClass with most result entries
1206
-	 * shall be pre-selected via the result
1207
-	 * @return array|false list of found items.
1208
-	 * @throws \Exception
1209
-	 */
1210
-	private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
1211
-		$cr = $this->getConnection();
1212
-		if (!$cr) {
1213
-			throw new \Exception('Could not connect to LDAP');
1214
-		}
1215
-		$p = 'objectclass=';
1216
-		foreach ($objectclasses as $key => $value) {
1217
-			$objectclasses[$key] = $p.$value;
1218
-		}
1219
-		$maxEntryObjC = '';
1220
-
1221
-		//how deep to dig?
1222
-		//When looking for objectclasses, testing few entries is sufficient,
1223
-		$dig = 3;
1224
-
1225
-		$availableFeatures =
1226
-			$this->cumulativeSearchOnAttribute($objectclasses, $attr,
1227
-											   $dig, $maxEntryObjC);
1228
-		if (is_array($availableFeatures)
1229
-		   && count($availableFeatures) > 0) {
1230
-			natcasesort($availableFeatures);
1231
-			//natcasesort keeps indices, but we must get rid of them for proper
1232
-			//sorting in the web UI. Therefore: array_values
1233
-			$this->result->addOptions($dbkey, array_values($availableFeatures));
1234
-		} else {
1235
-			throw new \Exception(self::$l->t('Could not find the desired feature'));
1236
-		}
1237
-
1238
-		$setFeatures = $this->configuration->$confkey;
1239
-		if (is_array($setFeatures) && !empty($setFeatures)) {
1240
-			//something is already configured? pre-select it.
1241
-			$this->result->addChange($dbkey, $setFeatures);
1242
-		} elseif ($po && $maxEntryObjC !== '') {
1243
-			//pre-select objectclass with most result entries
1244
-			$maxEntryObjC = str_replace($p, '', $maxEntryObjC);
1245
-			$this->applyFind($dbkey, $maxEntryObjC);
1246
-			$this->result->addChange($dbkey, $maxEntryObjC);
1247
-		}
1248
-
1249
-		return $availableFeatures;
1250
-	}
1251
-
1252
-	/**
1253
-	 * appends a list of values fr
1254
-	 * @param resource $result the return value from ldap_get_attributes
1255
-	 * @param string $attribute the attribute values to look for
1256
-	 * @param array &$known new values will be appended here
1257
-	 * @return int, state on of the class constants LRESULT_PROCESSED_OK,
1258
-	 * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
1259
-	 */
1260
-	private function getAttributeValuesFromEntry($result, $attribute, &$known) {
1261
-		if (!is_array($result)
1262
-		   || !isset($result['count'])
1263
-		   || !$result['count'] > 0) {
1264
-			return self::LRESULT_PROCESSED_INVALID;
1265
-		}
1266
-
1267
-		// strtolower on all keys for proper comparison
1268
-		$result = \OCP\Util::mb_array_change_key_case($result);
1269
-		$attribute = strtolower($attribute);
1270
-		if (isset($result[$attribute])) {
1271
-			foreach ($result[$attribute] as $key => $val) {
1272
-				if ($key === 'count') {
1273
-					continue;
1274
-				}
1275
-				if (!in_array($val, $known)) {
1276
-					$known[] = $val;
1277
-				}
1278
-			}
1279
-			return self::LRESULT_PROCESSED_OK;
1280
-		} else {
1281
-			return self::LRESULT_PROCESSED_SKIP;
1282
-		}
1283
-	}
1284
-
1285
-	/**
1286
-	 * @return bool|mixed
1287
-	 */
1288
-	private function getConnection() {
1289
-		if (!is_null($this->cr)) {
1290
-			return $this->cr;
1291
-		}
1292
-
1293
-		$cr = $this->ldap->connect(
1294
-			$this->configuration->ldapHost,
1295
-			$this->configuration->ldapPort
1296
-		);
1297
-
1298
-		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1299
-		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1300
-		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1301
-		if ($this->configuration->ldapTLS === 1) {
1302
-			$this->ldap->startTls($cr);
1303
-		}
1304
-
1305
-		$lo = @$this->ldap->bind($cr,
1306
-								 $this->configuration->ldapAgentName,
1307
-								 $this->configuration->ldapAgentPassword);
1308
-		if ($lo === true) {
1309
-			$this->$cr = $cr;
1310
-			return $cr;
1311
-		}
1312
-
1313
-		return false;
1314
-	}
1315
-
1316
-	/**
1317
-	 * @return array
1318
-	 */
1319
-	private function getDefaultLdapPortSettings() {
1320
-		static $settings = [
1321
-			['port' => 7636, 'tls' => false],
1322
-			['port' =>  636, 'tls' => false],
1323
-			['port' => 7389, 'tls' => true],
1324
-			['port' =>  389, 'tls' => true],
1325
-			['port' => 7389, 'tls' => false],
1326
-			['port' =>  389, 'tls' => false],
1327
-		];
1328
-		return $settings;
1329
-	}
1330
-
1331
-	/**
1332
-	 * @return array
1333
-	 */
1334
-	private function getPortSettingsToTry() {
1335
-		//389 ← LDAP / Unencrypted or StartTLS
1336
-		//636 ← LDAPS / SSL
1337
-		//7xxx ← UCS. need to be checked first, because both ports may be open
1338
-		$host = $this->configuration->ldapHost;
1339
-		$port = (int)$this->configuration->ldapPort;
1340
-		$portSettings = [];
1341
-
1342
-		//In case the port is already provided, we will check this first
1343
-		if ($port > 0) {
1344
-			$hostInfo = parse_url($host);
1345
-			if (!(is_array($hostInfo)
1346
-				&& isset($hostInfo['scheme'])
1347
-				&& stripos($hostInfo['scheme'], 'ldaps') !== false)) {
1348
-				$portSettings[] = ['port' => $port, 'tls' => true];
1349
-			}
1350
-			$portSettings[] =['port' => $port, 'tls' => false];
1351
-		}
1352
-
1353
-		//default ports
1354
-		$portSettings = array_merge($portSettings,
1355
-									$this->getDefaultLdapPortSettings());
1356
-
1357
-		return $portSettings;
1358
-	}
45
+    /** @var \OCP\IL10N */
46
+    protected static $l;
47
+    protected $access;
48
+    protected $cr;
49
+    protected $configuration;
50
+    protected $result;
51
+    protected $resultCache = [];
52
+
53
+    public const LRESULT_PROCESSED_OK = 2;
54
+    public const LRESULT_PROCESSED_INVALID = 3;
55
+    public const LRESULT_PROCESSED_SKIP = 4;
56
+
57
+    public const LFILTER_LOGIN      = 2;
58
+    public const LFILTER_USER_LIST  = 3;
59
+    public const LFILTER_GROUP_LIST = 4;
60
+
61
+    public const LFILTER_MODE_ASSISTED = 2;
62
+    public const LFILTER_MODE_RAW = 1;
63
+
64
+    public const LDAP_NW_TIMEOUT = 4;
65
+
66
+    /**
67
+     * Constructor
68
+     * @param Configuration $configuration an instance of Configuration
69
+     * @param ILDAPWrapper $ldap an instance of ILDAPWrapper
70
+     * @param Access $access
71
+     */
72
+    public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
73
+        parent::__construct($ldap);
74
+        $this->configuration = $configuration;
75
+        if (is_null(Wizard::$l)) {
76
+            Wizard::$l = \OC::$server->getL10N('user_ldap');
77
+        }
78
+        $this->access = $access;
79
+        $this->result = new WizardResult();
80
+    }
81
+
82
+    public function __destruct() {
83
+        if ($this->result->hasChanges()) {
84
+            $this->configuration->saveConfiguration();
85
+        }
86
+    }
87
+
88
+    /**
89
+     * counts entries in the LDAP directory
90
+     *
91
+     * @param string $filter the LDAP search filter
92
+     * @param string $type a string being either 'users' or 'groups';
93
+     * @return int
94
+     * @throws \Exception
95
+     */
96
+    public function countEntries(string $filter, string $type): int {
97
+        $reqs = ['ldapHost', 'ldapPort', 'ldapBase'];
98
+        if ($type === 'users') {
99
+            $reqs[] = 'ldapUserFilter';
100
+        }
101
+        if (!$this->checkRequirements($reqs)) {
102
+            throw new \Exception('Requirements not met', 400);
103
+        }
104
+
105
+        $attr = ['dn']; // default
106
+        $limit = 1001;
107
+        if ($type === 'groups') {
108
+            $result =  $this->access->countGroups($filter, $attr, $limit);
109
+        } elseif ($type === 'users') {
110
+            $result = $this->access->countUsers($filter, $attr, $limit);
111
+        } elseif ($type === 'objects') {
112
+            $result = $this->access->countObjects($limit);
113
+        } else {
114
+            throw new \Exception('Internal error: Invalid object type', 500);
115
+        }
116
+
117
+        return (int)$result;
118
+    }
119
+
120
+    /**
121
+     * formats the return value of a count operation to the string to be
122
+     * inserted.
123
+     *
124
+     * @param int $count
125
+     * @return string
126
+     */
127
+    private function formatCountResult(int $count): string {
128
+        if ($count > 1000) {
129
+            return '> 1000';
130
+        }
131
+        return (string)$count;
132
+    }
133
+
134
+    public function countGroups() {
135
+        $filter = $this->configuration->ldapGroupFilter;
136
+
137
+        if (empty($filter)) {
138
+            $output = self::$l->n('%s group found', '%s groups found', 0, [0]);
139
+            $this->result->addChange('ldap_group_count', $output);
140
+            return $this->result;
141
+        }
142
+
143
+        try {
144
+            $groupsTotal = $this->countEntries($filter, 'groups');
145
+        } catch (\Exception $e) {
146
+            //400 can be ignored, 500 is forwarded
147
+            if ($e->getCode() === 500) {
148
+                throw $e;
149
+            }
150
+            return false;
151
+        }
152
+        $output = self::$l->n(
153
+            '%s group found',
154
+            '%s groups found',
155
+            $groupsTotal,
156
+            [$this->formatCountResult($groupsTotal)]
157
+        );
158
+        $this->result->addChange('ldap_group_count', $output);
159
+        return $this->result;
160
+    }
161
+
162
+    /**
163
+     * @return WizardResult
164
+     * @throws \Exception
165
+     */
166
+    public function countUsers() {
167
+        $filter = $this->access->getFilterForUserCount();
168
+
169
+        $usersTotal = $this->countEntries($filter, 'users');
170
+        $output = self::$l->n(
171
+            '%s user found',
172
+            '%s users found',
173
+            $usersTotal,
174
+            [$this->formatCountResult($usersTotal)]
175
+        );
176
+        $this->result->addChange('ldap_user_count', $output);
177
+        return $this->result;
178
+    }
179
+
180
+    /**
181
+     * counts any objects in the currently set base dn
182
+     *
183
+     * @return WizardResult
184
+     * @throws \Exception
185
+     */
186
+    public function countInBaseDN() {
187
+        // we don't need to provide a filter in this case
188
+        $total = $this->countEntries('', 'objects');
189
+        if ($total === false) {
190
+            throw new \Exception('invalid results received');
191
+        }
192
+        $this->result->addChange('ldap_test_base', $total);
193
+        return $this->result;
194
+    }
195
+
196
+    /**
197
+     * counts users with a specified attribute
198
+     * @param string $attr
199
+     * @param bool $existsCheck
200
+     * @return int|bool
201
+     */
202
+    public function countUsersWithAttribute($attr, $existsCheck = false) {
203
+        if (!$this->checkRequirements(['ldapHost',
204
+            'ldapPort',
205
+            'ldapBase',
206
+            'ldapUserFilter',
207
+        ])) {
208
+            return  false;
209
+        }
210
+
211
+        $filter = $this->access->combineFilterWithAnd([
212
+            $this->configuration->ldapUserFilter,
213
+            $attr . '=*'
214
+        ]);
215
+
216
+        $limit = ($existsCheck === false) ? null : 1;
217
+
218
+        return $this->access->countUsers($filter, ['dn'], $limit);
219
+    }
220
+
221
+    /**
222
+     * detects the display name attribute. If a setting is already present that
223
+     * returns at least one hit, the detection will be canceled.
224
+     * @return WizardResult|bool
225
+     * @throws \Exception
226
+     */
227
+    public function detectUserDisplayNameAttribute() {
228
+        if (!$this->checkRequirements(['ldapHost',
229
+            'ldapPort',
230
+            'ldapBase',
231
+            'ldapUserFilter',
232
+        ])) {
233
+            return  false;
234
+        }
235
+
236
+        $attr = $this->configuration->ldapUserDisplayName;
237
+        if ($attr !== '' && $attr !== 'displayName') {
238
+            // most likely not the default value with upper case N,
239
+            // verify it still produces a result
240
+            $count = (int)$this->countUsersWithAttribute($attr, true);
241
+            if ($count > 0) {
242
+                //no change, but we sent it back to make sure the user interface
243
+                //is still correct, even if the ajax call was cancelled meanwhile
244
+                $this->result->addChange('ldap_display_name', $attr);
245
+                return $this->result;
246
+            }
247
+        }
248
+
249
+        // first attribute that has at least one result wins
250
+        $displayNameAttrs = ['displayname', 'cn'];
251
+        foreach ($displayNameAttrs as $attr) {
252
+            $count = (int)$this->countUsersWithAttribute($attr, true);
253
+
254
+            if ($count > 0) {
255
+                $this->applyFind('ldap_display_name', $attr);
256
+                return $this->result;
257
+            }
258
+        }
259
+
260
+        throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.'));
261
+    }
262
+
263
+    /**
264
+     * detects the most often used email attribute for users applying to the
265
+     * user list filter. If a setting is already present that returns at least
266
+     * one hit, the detection will be canceled.
267
+     * @return WizardResult|bool
268
+     */
269
+    public function detectEmailAttribute() {
270
+        if (!$this->checkRequirements(['ldapHost',
271
+            'ldapPort',
272
+            'ldapBase',
273
+            'ldapUserFilter',
274
+        ])) {
275
+            return  false;
276
+        }
277
+
278
+        $attr = $this->configuration->ldapEmailAttribute;
279
+        if ($attr !== '') {
280
+            $count = (int)$this->countUsersWithAttribute($attr, true);
281
+            if ($count > 0) {
282
+                return false;
283
+            }
284
+            $writeLog = true;
285
+        } else {
286
+            $writeLog = false;
287
+        }
288
+
289
+        $emailAttributes = ['mail', 'mailPrimaryAddress'];
290
+        $winner = '';
291
+        $maxUsers = 0;
292
+        foreach ($emailAttributes as $attr) {
293
+            $count = $this->countUsersWithAttribute($attr);
294
+            if ($count > $maxUsers) {
295
+                $maxUsers = $count;
296
+                $winner = $attr;
297
+            }
298
+        }
299
+
300
+        if ($winner !== '') {
301
+            $this->applyFind('ldap_email_attr', $winner);
302
+            if ($writeLog) {
303
+                \OCP\Util::writeLog('user_ldap', 'The mail attribute has ' .
304
+                    'automatically been reset, because the original value ' .
305
+                    'did not return any results.', ILogger::INFO);
306
+            }
307
+        }
308
+
309
+        return $this->result;
310
+    }
311
+
312
+    /**
313
+     * @return WizardResult
314
+     * @throws \Exception
315
+     */
316
+    public function determineAttributes() {
317
+        if (!$this->checkRequirements(['ldapHost',
318
+            'ldapPort',
319
+            'ldapBase',
320
+            'ldapUserFilter',
321
+        ])) {
322
+            return  false;
323
+        }
324
+
325
+        $attributes = $this->getUserAttributes();
326
+
327
+        natcasesort($attributes);
328
+        $attributes = array_values($attributes);
329
+
330
+        $this->result->addOptions('ldap_loginfilter_attributes', $attributes);
331
+
332
+        $selected = $this->configuration->ldapLoginFilterAttributes;
333
+        if (is_array($selected) && !empty($selected)) {
334
+            $this->result->addChange('ldap_loginfilter_attributes', $selected);
335
+        }
336
+
337
+        return $this->result;
338
+    }
339
+
340
+    /**
341
+     * detects the available LDAP attributes
342
+     * @return array|false The instance's WizardResult instance
343
+     * @throws \Exception
344
+     */
345
+    private function getUserAttributes() {
346
+        if (!$this->checkRequirements(['ldapHost',
347
+            'ldapPort',
348
+            'ldapBase',
349
+            'ldapUserFilter',
350
+        ])) {
351
+            return  false;
352
+        }
353
+        $cr = $this->getConnection();
354
+        if (!$cr) {
355
+            throw new \Exception('Could not connect to LDAP');
356
+        }
357
+
358
+        $base = $this->configuration->ldapBase[0];
359
+        $filter = $this->configuration->ldapUserFilter;
360
+        $rr = $this->ldap->search($cr, $base, $filter, [], 1, 1);
361
+        if (!$this->ldap->isResource($rr)) {
362
+            return false;
363
+        }
364
+        $er = $this->ldap->firstEntry($cr, $rr);
365
+        $attributes = $this->ldap->getAttributes($cr, $er);
366
+        $pureAttributes = [];
367
+        for ($i = 0; $i < $attributes['count']; $i++) {
368
+            $pureAttributes[] = $attributes[$i];
369
+        }
370
+
371
+        return $pureAttributes;
372
+    }
373
+
374
+    /**
375
+     * detects the available LDAP groups
376
+     * @return WizardResult|false the instance's WizardResult instance
377
+     */
378
+    public function determineGroupsForGroups() {
379
+        return $this->determineGroups('ldap_groupfilter_groups',
380
+                                        'ldapGroupFilterGroups',
381
+                                        false);
382
+    }
383
+
384
+    /**
385
+     * detects the available LDAP groups
386
+     * @return WizardResult|false the instance's WizardResult instance
387
+     */
388
+    public function determineGroupsForUsers() {
389
+        return $this->determineGroups('ldap_userfilter_groups',
390
+                                        'ldapUserFilterGroups');
391
+    }
392
+
393
+    /**
394
+     * detects the available LDAP groups
395
+     * @param string $dbKey
396
+     * @param string $confKey
397
+     * @param bool $testMemberOf
398
+     * @return WizardResult|false the instance's WizardResult instance
399
+     * @throws \Exception
400
+     */
401
+    private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
402
+        if (!$this->checkRequirements(['ldapHost',
403
+            'ldapPort',
404
+            'ldapBase',
405
+        ])) {
406
+            return  false;
407
+        }
408
+        $cr = $this->getConnection();
409
+        if (!$cr) {
410
+            throw new \Exception('Could not connect to LDAP');
411
+        }
412
+
413
+        $this->fetchGroups($dbKey, $confKey);
414
+
415
+        if ($testMemberOf) {
416
+            $this->configuration->hasMemberOfFilterSupport = $this->testMemberOf();
417
+            $this->result->markChange();
418
+            if (!$this->configuration->hasMemberOfFilterSupport) {
419
+                throw new \Exception('memberOf is not supported by the server');
420
+            }
421
+        }
422
+
423
+        return $this->result;
424
+    }
425
+
426
+    /**
427
+     * fetches all groups from LDAP and adds them to the result object
428
+     *
429
+     * @param string $dbKey
430
+     * @param string $confKey
431
+     * @return array $groupEntries
432
+     * @throws \Exception
433
+     */
434
+    public function fetchGroups($dbKey, $confKey) {
435
+        $obclasses = ['posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames'];
436
+
437
+        $filterParts = [];
438
+        foreach ($obclasses as $obclass) {
439
+            $filterParts[] = 'objectclass='.$obclass;
440
+        }
441
+        //we filter for everything
442
+        //- that looks like a group and
443
+        //- has the group display name set
444
+        $filter = $this->access->combineFilterWithOr($filterParts);
445
+        $filter = $this->access->combineFilterWithAnd([$filter, 'cn=*']);
446
+
447
+        $groupNames = [];
448
+        $groupEntries = [];
449
+        $limit = 400;
450
+        $offset = 0;
451
+        do {
452
+            // we need to request dn additionally here, otherwise memberOf
453
+            // detection will fail later
454
+            $result = $this->access->searchGroups($filter, ['cn', 'dn'], $limit, $offset);
455
+            foreach ($result as $item) {
456
+                if (!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) {
457
+                    // just in case - no issue known
458
+                    continue;
459
+                }
460
+                $groupNames[] = $item['cn'][0];
461
+                $groupEntries[] = $item;
462
+            }
463
+            $offset += $limit;
464
+        } while ($this->access->hasMoreResults());
465
+
466
+        if (count($groupNames) > 0) {
467
+            natsort($groupNames);
468
+            $this->result->addOptions($dbKey, array_values($groupNames));
469
+        } else {
470
+            throw new \Exception(self::$l->t('Could not find the desired feature'));
471
+        }
472
+
473
+        $setFeatures = $this->configuration->$confKey;
474
+        if (is_array($setFeatures) && !empty($setFeatures)) {
475
+            //something is already configured? pre-select it.
476
+            $this->result->addChange($dbKey, $setFeatures);
477
+        }
478
+        return $groupEntries;
479
+    }
480
+
481
+    public function determineGroupMemberAssoc() {
482
+        if (!$this->checkRequirements(['ldapHost',
483
+            'ldapPort',
484
+            'ldapGroupFilter',
485
+        ])) {
486
+            return  false;
487
+        }
488
+        $attribute = $this->detectGroupMemberAssoc();
489
+        if ($attribute === false) {
490
+            return false;
491
+        }
492
+        $this->configuration->setConfiguration(['ldapGroupMemberAssocAttr' => $attribute]);
493
+        $this->result->addChange('ldap_group_member_assoc_attribute', $attribute);
494
+
495
+        return $this->result;
496
+    }
497
+
498
+    /**
499
+     * Detects the available object classes
500
+     * @return WizardResult|false the instance's WizardResult instance
501
+     * @throws \Exception
502
+     */
503
+    public function determineGroupObjectClasses() {
504
+        if (!$this->checkRequirements(['ldapHost',
505
+            'ldapPort',
506
+            'ldapBase',
507
+        ])) {
508
+            return  false;
509
+        }
510
+        $cr = $this->getConnection();
511
+        if (!$cr) {
512
+            throw new \Exception('Could not connect to LDAP');
513
+        }
514
+
515
+        $obclasses = ['groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*'];
516
+        $this->determineFeature($obclasses,
517
+                                'objectclass',
518
+                                'ldap_groupfilter_objectclass',
519
+                                'ldapGroupFilterObjectclass',
520
+                                false);
521
+
522
+        return $this->result;
523
+    }
524
+
525
+    /**
526
+     * detects the available object classes
527
+     * @return WizardResult
528
+     * @throws \Exception
529
+     */
530
+    public function determineUserObjectClasses() {
531
+        if (!$this->checkRequirements(['ldapHost',
532
+            'ldapPort',
533
+            'ldapBase',
534
+        ])) {
535
+            return  false;
536
+        }
537
+        $cr = $this->getConnection();
538
+        if (!$cr) {
539
+            throw new \Exception('Could not connect to LDAP');
540
+        }
541
+
542
+        $obclasses = ['inetOrgPerson', 'person', 'organizationalPerson',
543
+            'user', 'posixAccount', '*'];
544
+        $filter = $this->configuration->ldapUserFilter;
545
+        //if filter is empty, it is probably the first time the wizard is called
546
+        //then, apply suggestions.
547
+        $this->determineFeature($obclasses,
548
+                                'objectclass',
549
+                                'ldap_userfilter_objectclass',
550
+                                'ldapUserFilterObjectclass',
551
+                                empty($filter));
552
+
553
+        return $this->result;
554
+    }
555
+
556
+    /**
557
+     * @return WizardResult|false
558
+     * @throws \Exception
559
+     */
560
+    public function getGroupFilter() {
561
+        if (!$this->checkRequirements(['ldapHost',
562
+            'ldapPort',
563
+            'ldapBase',
564
+        ])) {
565
+            return false;
566
+        }
567
+        //make sure the use display name is set
568
+        $displayName = $this->configuration->ldapGroupDisplayName;
569
+        if ($displayName === '') {
570
+            $d = $this->configuration->getDefaults();
571
+            $this->applyFind('ldap_group_display_name',
572
+                                $d['ldap_group_display_name']);
573
+        }
574
+        $filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
575
+
576
+        $this->applyFind('ldap_group_filter', $filter);
577
+        return $this->result;
578
+    }
579
+
580
+    /**
581
+     * @return WizardResult|false
582
+     * @throws \Exception
583
+     */
584
+    public function getUserListFilter() {
585
+        if (!$this->checkRequirements(['ldapHost',
586
+            'ldapPort',
587
+            'ldapBase',
588
+        ])) {
589
+            return false;
590
+        }
591
+        //make sure the use display name is set
592
+        $displayName = $this->configuration->ldapUserDisplayName;
593
+        if ($displayName === '') {
594
+            $d = $this->configuration->getDefaults();
595
+            $this->applyFind('ldap_display_name', $d['ldap_display_name']);
596
+        }
597
+        $filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
598
+        if (!$filter) {
599
+            throw new \Exception('Cannot create filter');
600
+        }
601
+
602
+        $this->applyFind('ldap_userlist_filter', $filter);
603
+        return $this->result;
604
+    }
605
+
606
+    /**
607
+     * @return bool|WizardResult
608
+     * @throws \Exception
609
+     */
610
+    public function getUserLoginFilter() {
611
+        if (!$this->checkRequirements(['ldapHost',
612
+            'ldapPort',
613
+            'ldapBase',
614
+            'ldapUserFilter',
615
+        ])) {
616
+            return false;
617
+        }
618
+
619
+        $filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
620
+        if (!$filter) {
621
+            throw new \Exception('Cannot create filter');
622
+        }
623
+
624
+        $this->applyFind('ldap_login_filter', $filter);
625
+        return $this->result;
626
+    }
627
+
628
+    /**
629
+     * @return bool|WizardResult
630
+     * @param string $loginName
631
+     * @throws \Exception
632
+     */
633
+    public function testLoginName($loginName) {
634
+        if (!$this->checkRequirements(['ldapHost',
635
+            'ldapPort',
636
+            'ldapBase',
637
+            'ldapLoginFilter',
638
+        ])) {
639
+            return false;
640
+        }
641
+
642
+        $cr = $this->access->connection->getConnectionResource();
643
+        if (!$this->ldap->isResource($cr)) {
644
+            throw new \Exception('connection error');
645
+        }
646
+
647
+        if (mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8')
648
+            === false) {
649
+            throw new \Exception('missing placeholder');
650
+        }
651
+
652
+        $users = $this->access->countUsersByLoginName($loginName);
653
+        if ($this->ldap->errno($cr) !== 0) {
654
+            throw new \Exception($this->ldap->error($cr));
655
+        }
656
+        $filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter);
657
+        $this->result->addChange('ldap_test_loginname', $users);
658
+        $this->result->addChange('ldap_test_effective_filter', $filter);
659
+        return $this->result;
660
+    }
661
+
662
+    /**
663
+     * Tries to determine the port, requires given Host, User DN and Password
664
+     * @return WizardResult|false WizardResult on success, false otherwise
665
+     * @throws \Exception
666
+     */
667
+    public function guessPortAndTLS() {
668
+        if (!$this->checkRequirements(['ldapHost',
669
+        ])) {
670
+            return false;
671
+        }
672
+        $this->checkHost();
673
+        $portSettings = $this->getPortSettingsToTry();
674
+
675
+        if (!is_array($portSettings)) {
676
+            throw new \Exception(print_r($portSettings, true));
677
+        }
678
+
679
+        //proceed from the best configuration and return on first success
680
+        foreach ($portSettings as $setting) {
681
+            $p = $setting['port'];
682
+            $t = $setting['tls'];
683
+            \OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, ILogger::DEBUG);
684
+            //connectAndBind may throw Exception, it needs to be catched by the
685
+            //callee of this method
686
+
687
+            try {
688
+                $settingsFound = $this->connectAndBind($p, $t);
689
+            } catch (\Exception $e) {
690
+                // any reply other than -1 (= cannot connect) is already okay,
691
+                // because then we found the server
692
+                // unavailable startTLS returns -11
693
+                if ($e->getCode() > 0) {
694
+                    $settingsFound = true;
695
+                } else {
696
+                    throw $e;
697
+                }
698
+            }
699
+
700
+            if ($settingsFound === true) {
701
+                $config = [
702
+                    'ldapPort' => $p,
703
+                    'ldapTLS' => (int)$t
704
+                ];
705
+                $this->configuration->setConfiguration($config);
706
+                \OCP\Util::writeLog('user_ldap', 'Wiz: detected Port ' . $p, ILogger::DEBUG);
707
+                $this->result->addChange('ldap_port', $p);
708
+                return $this->result;
709
+            }
710
+        }
711
+
712
+        //custom port, undetected (we do not brute force)
713
+        return false;
714
+    }
715
+
716
+    /**
717
+     * tries to determine a base dn from User DN or LDAP Host
718
+     * @return WizardResult|false WizardResult on success, false otherwise
719
+     */
720
+    public function guessBaseDN() {
721
+        if (!$this->checkRequirements(['ldapHost',
722
+            'ldapPort',
723
+        ])) {
724
+            return false;
725
+        }
726
+
727
+        //check whether a DN is given in the agent name (99.9% of all cases)
728
+        $base = null;
729
+        $i = stripos($this->configuration->ldapAgentName, 'dc=');
730
+        if ($i !== false) {
731
+            $base = substr($this->configuration->ldapAgentName, $i);
732
+            if ($this->testBaseDN($base)) {
733
+                $this->applyFind('ldap_base', $base);
734
+                return $this->result;
735
+            }
736
+        }
737
+
738
+        //this did not help :(
739
+        //Let's see whether we can parse the Host URL and convert the domain to
740
+        //a base DN
741
+        $helper = new Helper(\OC::$server->getConfig());
742
+        $domain = $helper->getDomainFromURL($this->configuration->ldapHost);
743
+        if (!$domain) {
744
+            return false;
745
+        }
746
+
747
+        $dparts = explode('.', $domain);
748
+        while (count($dparts) > 0) {
749
+            $base2 = 'dc=' . implode(',dc=', $dparts);
750
+            if ($base !== $base2 && $this->testBaseDN($base2)) {
751
+                $this->applyFind('ldap_base', $base2);
752
+                return $this->result;
753
+            }
754
+            array_shift($dparts);
755
+        }
756
+
757
+        return false;
758
+    }
759
+
760
+    /**
761
+     * sets the found value for the configuration key in the WizardResult
762
+     * as well as in the Configuration instance
763
+     * @param string $key the configuration key
764
+     * @param string $value the (detected) value
765
+     *
766
+     */
767
+    private function applyFind($key, $value) {
768
+        $this->result->addChange($key, $value);
769
+        $this->configuration->setConfiguration([$key => $value]);
770
+    }
771
+
772
+    /**
773
+     * Checks, whether a port was entered in the Host configuration
774
+     * field. In this case the port will be stripped off, but also stored as
775
+     * setting.
776
+     */
777
+    private function checkHost() {
778
+        $host = $this->configuration->ldapHost;
779
+        $hostInfo = parse_url($host);
780
+
781
+        //removes Port from Host
782
+        if (is_array($hostInfo) && isset($hostInfo['port'])) {
783
+            $port = $hostInfo['port'];
784
+            $host = str_replace(':'.$port, '', $host);
785
+            $this->applyFind('ldap_host', $host);
786
+            $this->applyFind('ldap_port', $port);
787
+        }
788
+    }
789
+
790
+    /**
791
+     * tries to detect the group member association attribute which is
792
+     * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
793
+     * @return string|false, string with the attribute name, false on error
794
+     * @throws \Exception
795
+     */
796
+    private function detectGroupMemberAssoc() {
797
+        $possibleAttrs = ['uniqueMember', 'memberUid', 'member', 'gidNumber', 'zimbraMailForwardingAddress'];
798
+        $filter = $this->configuration->ldapGroupFilter;
799
+        if (empty($filter)) {
800
+            return false;
801
+        }
802
+        $cr = $this->getConnection();
803
+        if (!$cr) {
804
+            throw new \Exception('Could not connect to LDAP');
805
+        }
806
+        $base = $this->configuration->ldapBaseGroups[0] ?: $this->configuration->ldapBase[0];
807
+        $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000);
808
+        if (!$this->ldap->isResource($rr)) {
809
+            return false;
810
+        }
811
+        $er = $this->ldap->firstEntry($cr, $rr);
812
+        while (is_resource($er)) {
813
+            $this->ldap->getDN($cr, $er);
814
+            $attrs = $this->ldap->getAttributes($cr, $er);
815
+            $result = [];
816
+            $possibleAttrsCount = count($possibleAttrs);
817
+            for ($i = 0; $i < $possibleAttrsCount; $i++) {
818
+                if (isset($attrs[$possibleAttrs[$i]])) {
819
+                    $result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
820
+                }
821
+            }
822
+            if (!empty($result)) {
823
+                natsort($result);
824
+                return key($result);
825
+            }
826
+
827
+            $er = $this->ldap->nextEntry($cr, $er);
828
+        }
829
+
830
+        return false;
831
+    }
832
+
833
+    /**
834
+     * Checks whether for a given BaseDN results will be returned
835
+     * @param string $base the BaseDN to test
836
+     * @return bool true on success, false otherwise
837
+     * @throws \Exception
838
+     */
839
+    private function testBaseDN($base) {
840
+        $cr = $this->getConnection();
841
+        if (!$cr) {
842
+            throw new \Exception('Could not connect to LDAP');
843
+        }
844
+
845
+        //base is there, let's validate it. If we search for anything, we should
846
+        //get a result set > 0 on a proper base
847
+        $rr = $this->ldap->search($cr, $base, 'objectClass=*', ['dn'], 0, 1);
848
+        if (!$this->ldap->isResource($rr)) {
849
+            $errorNo  = $this->ldap->errno($cr);
850
+            $errorMsg = $this->ldap->error($cr);
851
+            \OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base.
852
+                            ' Error '.$errorNo.': '.$errorMsg, ILogger::INFO);
853
+            return false;
854
+        }
855
+        $entries = $this->ldap->countEntries($cr, $rr);
856
+        return ($entries !== false) && ($entries > 0);
857
+    }
858
+
859
+    /**
860
+     * Checks whether the server supports memberOf in LDAP Filter.
861
+     * Note: at least in OpenLDAP, availability of memberOf is dependent on
862
+     * a configured objectClass. I.e. not necessarily for all available groups
863
+     * memberOf does work.
864
+     *
865
+     * @return bool true if it does, false otherwise
866
+     * @throws \Exception
867
+     */
868
+    private function testMemberOf() {
869
+        $cr = $this->getConnection();
870
+        if (!$cr) {
871
+            throw new \Exception('Could not connect to LDAP');
872
+        }
873
+        $result = $this->access->countUsers('memberOf=*', ['memberOf'], 1);
874
+        if (is_int($result) &&  $result > 0) {
875
+            return true;
876
+        }
877
+        return false;
878
+    }
879
+
880
+    /**
881
+     * creates an LDAP Filter from given configuration
882
+     * @param integer $filterType int, for which use case the filter shall be created
883
+     * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
884
+     * self::LFILTER_GROUP_LIST
885
+     * @return string|false string with the filter on success, false otherwise
886
+     * @throws \Exception
887
+     */
888
+    private function composeLdapFilter($filterType) {
889
+        $filter = '';
890
+        $parts = 0;
891
+        switch ($filterType) {
892
+            case self::LFILTER_USER_LIST:
893
+                $objcs = $this->configuration->ldapUserFilterObjectclass;
894
+                //glue objectclasses
895
+                if (is_array($objcs) && count($objcs) > 0) {
896
+                    $filter .= '(|';
897
+                    foreach ($objcs as $objc) {
898
+                        $filter .= '(objectclass=' . $objc . ')';
899
+                    }
900
+                    $filter .= ')';
901
+                    $parts++;
902
+                }
903
+                //glue group memberships
904
+                if ($this->configuration->hasMemberOfFilterSupport) {
905
+                    $cns = $this->configuration->ldapUserFilterGroups;
906
+                    if (is_array($cns) && count($cns) > 0) {
907
+                        $filter .= '(|';
908
+                        $cr = $this->getConnection();
909
+                        if (!$cr) {
910
+                            throw new \Exception('Could not connect to LDAP');
911
+                        }
912
+                        $base = $this->configuration->ldapBase[0];
913
+                        foreach ($cns as $cn) {
914
+                            $rr = $this->ldap->search($cr, $base, 'cn=' . $cn, ['dn', 'primaryGroupToken']);
915
+                            if (!$this->ldap->isResource($rr)) {
916
+                                continue;
917
+                            }
918
+                            $er = $this->ldap->firstEntry($cr, $rr);
919
+                            $attrs = $this->ldap->getAttributes($cr, $er);
920
+                            $dn = $this->ldap->getDN($cr, $er);
921
+                            if ($dn === false || $dn === '') {
922
+                                continue;
923
+                            }
924
+                            $filterPart = '(memberof=' . $dn . ')';
925
+                            if (isset($attrs['primaryGroupToken'])) {
926
+                                $pgt = $attrs['primaryGroupToken'][0];
927
+                                $primaryFilterPart = '(primaryGroupID=' . $pgt .')';
928
+                                $filterPart = '(|' . $filterPart . $primaryFilterPart . ')';
929
+                            }
930
+                            $filter .= $filterPart;
931
+                        }
932
+                        $filter .= ')';
933
+                    }
934
+                    $parts++;
935
+                }
936
+                //wrap parts in AND condition
937
+                if ($parts > 1) {
938
+                    $filter = '(&' . $filter . ')';
939
+                }
940
+                if ($filter === '') {
941
+                    $filter = '(objectclass=*)';
942
+                }
943
+                break;
944
+
945
+            case self::LFILTER_GROUP_LIST:
946
+                $objcs = $this->configuration->ldapGroupFilterObjectclass;
947
+                //glue objectclasses
948
+                if (is_array($objcs) && count($objcs) > 0) {
949
+                    $filter .= '(|';
950
+                    foreach ($objcs as $objc) {
951
+                        $filter .= '(objectclass=' . $objc . ')';
952
+                    }
953
+                    $filter .= ')';
954
+                    $parts++;
955
+                }
956
+                //glue group memberships
957
+                $cns = $this->configuration->ldapGroupFilterGroups;
958
+                if (is_array($cns) && count($cns) > 0) {
959
+                    $filter .= '(|';
960
+                    foreach ($cns as $cn) {
961
+                        $filter .= '(cn=' . $cn . ')';
962
+                    }
963
+                    $filter .= ')';
964
+                }
965
+                $parts++;
966
+                //wrap parts in AND condition
967
+                if ($parts > 1) {
968
+                    $filter = '(&' . $filter . ')';
969
+                }
970
+                break;
971
+
972
+            case self::LFILTER_LOGIN:
973
+                $ulf = $this->configuration->ldapUserFilter;
974
+                $loginpart = '=%uid';
975
+                $filterUsername = '';
976
+                $userAttributes = $this->getUserAttributes();
977
+                $userAttributes = array_change_key_case(array_flip($userAttributes));
978
+                $parts = 0;
979
+
980
+                if ($this->configuration->ldapLoginFilterUsername === '1') {
981
+                    $attr = '';
982
+                    if (isset($userAttributes['uid'])) {
983
+                        $attr = 'uid';
984
+                    } elseif (isset($userAttributes['samaccountname'])) {
985
+                        $attr = 'samaccountname';
986
+                    } elseif (isset($userAttributes['cn'])) {
987
+                        //fallback
988
+                        $attr = 'cn';
989
+                    }
990
+                    if ($attr !== '') {
991
+                        $filterUsername = '(' . $attr . $loginpart . ')';
992
+                        $parts++;
993
+                    }
994
+                }
995
+
996
+                $filterEmail = '';
997
+                if ($this->configuration->ldapLoginFilterEmail === '1') {
998
+                    $filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
999
+                    $parts++;
1000
+                }
1001
+
1002
+                $filterAttributes = '';
1003
+                $attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
1004
+                if (is_array($attrsToFilter) && count($attrsToFilter) > 0) {
1005
+                    $filterAttributes = '(|';
1006
+                    foreach ($attrsToFilter as $attribute) {
1007
+                        $filterAttributes .= '(' . $attribute . $loginpart . ')';
1008
+                    }
1009
+                    $filterAttributes .= ')';
1010
+                    $parts++;
1011
+                }
1012
+
1013
+                $filterLogin = '';
1014
+                if ($parts > 1) {
1015
+                    $filterLogin = '(|';
1016
+                }
1017
+                $filterLogin .= $filterUsername;
1018
+                $filterLogin .= $filterEmail;
1019
+                $filterLogin .= $filterAttributes;
1020
+                if ($parts > 1) {
1021
+                    $filterLogin .= ')';
1022
+                }
1023
+
1024
+                $filter = '(&'.$ulf.$filterLogin.')';
1025
+                break;
1026
+        }
1027
+
1028
+        \OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, ILogger::DEBUG);
1029
+
1030
+        return $filter;
1031
+    }
1032
+
1033
+    /**
1034
+     * Connects and Binds to an LDAP Server
1035
+     *
1036
+     * @param int $port the port to connect with
1037
+     * @param bool $tls whether startTLS is to be used
1038
+     * @return bool
1039
+     * @throws \Exception
1040
+     */
1041
+    private function connectAndBind($port, $tls) {
1042
+        //connect, does not really trigger any server communication
1043
+        $host = $this->configuration->ldapHost;
1044
+        $hostInfo = parse_url($host);
1045
+        if (!$hostInfo) {
1046
+            throw new \Exception(self::$l->t('Invalid Host'));
1047
+        }
1048
+        \OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', ILogger::DEBUG);
1049
+        $cr = $this->ldap->connect($host, $port);
1050
+        if (!is_resource($cr)) {
1051
+            throw new \Exception(self::$l->t('Invalid Host'));
1052
+        }
1053
+
1054
+        //set LDAP options
1055
+        $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1056
+        $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1057
+        $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1058
+
1059
+        try {
1060
+            if ($tls) {
1061
+                $isTlsWorking = @$this->ldap->startTls($cr);
1062
+                if (!$isTlsWorking) {
1063
+                    return false;
1064
+                }
1065
+            }
1066
+
1067
+            \OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', ILogger::DEBUG);
1068
+            //interesting part: do the bind!
1069
+            $login = $this->ldap->bind($cr,
1070
+                $this->configuration->ldapAgentName,
1071
+                $this->configuration->ldapAgentPassword
1072
+            );
1073
+            $errNo = $this->ldap->errno($cr);
1074
+            $error = ldap_error($cr);
1075
+            $this->ldap->unbind($cr);
1076
+        } catch (ServerNotAvailableException $e) {
1077
+            return false;
1078
+        }
1079
+
1080
+        if ($login === true) {
1081
+            $this->ldap->unbind($cr);
1082
+            \OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, ILogger::DEBUG);
1083
+            return true;
1084
+        }
1085
+
1086
+        if ($errNo === -1) {
1087
+            //host, port or TLS wrong
1088
+            return false;
1089
+        }
1090
+        throw new \Exception($error, $errNo);
1091
+    }
1092
+
1093
+    /**
1094
+     * checks whether a valid combination of agent and password has been
1095
+     * provided (either two values or nothing for anonymous connect)
1096
+     * @return bool, true if everything is fine, false otherwise
1097
+     */
1098
+    private function checkAgentRequirements() {
1099
+        $agent = $this->configuration->ldapAgentName;
1100
+        $pwd = $this->configuration->ldapAgentPassword;
1101
+
1102
+        return
1103
+            ($agent !== '' && $pwd !== '')
1104
+            ||  ($agent === '' && $pwd === '')
1105
+        ;
1106
+    }
1107
+
1108
+    /**
1109
+     * @param array $reqs
1110
+     * @return bool
1111
+     */
1112
+    private function checkRequirements($reqs) {
1113
+        $this->checkAgentRequirements();
1114
+        foreach ($reqs as $option) {
1115
+            $value = $this->configuration->$option;
1116
+            if (empty($value)) {
1117
+                return false;
1118
+            }
1119
+        }
1120
+        return true;
1121
+    }
1122
+
1123
+    /**
1124
+     * does a cumulativeSearch on LDAP to get different values of a
1125
+     * specified attribute
1126
+     * @param string[] $filters array, the filters that shall be used in the search
1127
+     * @param string $attr the attribute of which a list of values shall be returned
1128
+     * @param int $dnReadLimit the amount of how many DNs should be analyzed.
1129
+     * The lower, the faster
1130
+     * @param string $maxF string. if not null, this variable will have the filter that
1131
+     * yields most result entries
1132
+     * @return array|false an array with the values on success, false otherwise
1133
+     */
1134
+    public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
1135
+        $dnRead = [];
1136
+        $foundItems = [];
1137
+        $maxEntries = 0;
1138
+        if (!is_array($this->configuration->ldapBase)
1139
+           || !isset($this->configuration->ldapBase[0])) {
1140
+            return false;
1141
+        }
1142
+        $base = $this->configuration->ldapBase[0];
1143
+        $cr = $this->getConnection();
1144
+        if (!$this->ldap->isResource($cr)) {
1145
+            return false;
1146
+        }
1147
+        $lastFilter = null;
1148
+        if (isset($filters[count($filters)-1])) {
1149
+            $lastFilter = $filters[count($filters)-1];
1150
+        }
1151
+        foreach ($filters as $filter) {
1152
+            if ($lastFilter === $filter && count($foundItems) > 0) {
1153
+                //skip when the filter is a wildcard and results were found
1154
+                continue;
1155
+            }
1156
+            // 20k limit for performance and reason
1157
+            $rr = $this->ldap->search($cr, $base, $filter, [$attr], 0, 20000);
1158
+            if (!$this->ldap->isResource($rr)) {
1159
+                continue;
1160
+            }
1161
+            $entries = $this->ldap->countEntries($cr, $rr);
1162
+            $getEntryFunc = 'firstEntry';
1163
+            if (($entries !== false) && ($entries > 0)) {
1164
+                if (!is_null($maxF) && $entries > $maxEntries) {
1165
+                    $maxEntries = $entries;
1166
+                    $maxF = $filter;
1167
+                }
1168
+                $dnReadCount = 0;
1169
+                do {
1170
+                    $entry = $this->ldap->$getEntryFunc($cr, $rr);
1171
+                    $getEntryFunc = 'nextEntry';
1172
+                    if (!$this->ldap->isResource($entry)) {
1173
+                        continue 2;
1174
+                    }
1175
+                    $rr = $entry; //will be expected by nextEntry next round
1176
+                    $attributes = $this->ldap->getAttributes($cr, $entry);
1177
+                    $dn = $this->ldap->getDN($cr, $entry);
1178
+                    if ($dn === false || in_array($dn, $dnRead)) {
1179
+                        continue;
1180
+                    }
1181
+                    $newItems = [];
1182
+                    $state = $this->getAttributeValuesFromEntry($attributes,
1183
+                                                                $attr,
1184
+                                                                $newItems);
1185
+                    $dnReadCount++;
1186
+                    $foundItems = array_merge($foundItems, $newItems);
1187
+                    $this->resultCache[$dn][$attr] = $newItems;
1188
+                    $dnRead[] = $dn;
1189
+                } while (($state === self::LRESULT_PROCESSED_SKIP
1190
+                        || $this->ldap->isResource($entry))
1191
+                        && ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
1192
+            }
1193
+        }
1194
+
1195
+        return array_unique($foundItems);
1196
+    }
1197
+
1198
+    /**
1199
+     * determines if and which $attr are available on the LDAP server
1200
+     * @param string[] $objectclasses the objectclasses to use as search filter
1201
+     * @param string $attr the attribute to look for
1202
+     * @param string $dbkey the dbkey of the setting the feature is connected to
1203
+     * @param string $confkey the confkey counterpart for the $dbkey as used in the
1204
+     * Configuration class
1205
+     * @param bool $po whether the objectClass with most result entries
1206
+     * shall be pre-selected via the result
1207
+     * @return array|false list of found items.
1208
+     * @throws \Exception
1209
+     */
1210
+    private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
1211
+        $cr = $this->getConnection();
1212
+        if (!$cr) {
1213
+            throw new \Exception('Could not connect to LDAP');
1214
+        }
1215
+        $p = 'objectclass=';
1216
+        foreach ($objectclasses as $key => $value) {
1217
+            $objectclasses[$key] = $p.$value;
1218
+        }
1219
+        $maxEntryObjC = '';
1220
+
1221
+        //how deep to dig?
1222
+        //When looking for objectclasses, testing few entries is sufficient,
1223
+        $dig = 3;
1224
+
1225
+        $availableFeatures =
1226
+            $this->cumulativeSearchOnAttribute($objectclasses, $attr,
1227
+                                                $dig, $maxEntryObjC);
1228
+        if (is_array($availableFeatures)
1229
+           && count($availableFeatures) > 0) {
1230
+            natcasesort($availableFeatures);
1231
+            //natcasesort keeps indices, but we must get rid of them for proper
1232
+            //sorting in the web UI. Therefore: array_values
1233
+            $this->result->addOptions($dbkey, array_values($availableFeatures));
1234
+        } else {
1235
+            throw new \Exception(self::$l->t('Could not find the desired feature'));
1236
+        }
1237
+
1238
+        $setFeatures = $this->configuration->$confkey;
1239
+        if (is_array($setFeatures) && !empty($setFeatures)) {
1240
+            //something is already configured? pre-select it.
1241
+            $this->result->addChange($dbkey, $setFeatures);
1242
+        } elseif ($po && $maxEntryObjC !== '') {
1243
+            //pre-select objectclass with most result entries
1244
+            $maxEntryObjC = str_replace($p, '', $maxEntryObjC);
1245
+            $this->applyFind($dbkey, $maxEntryObjC);
1246
+            $this->result->addChange($dbkey, $maxEntryObjC);
1247
+        }
1248
+
1249
+        return $availableFeatures;
1250
+    }
1251
+
1252
+    /**
1253
+     * appends a list of values fr
1254
+     * @param resource $result the return value from ldap_get_attributes
1255
+     * @param string $attribute the attribute values to look for
1256
+     * @param array &$known new values will be appended here
1257
+     * @return int, state on of the class constants LRESULT_PROCESSED_OK,
1258
+     * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
1259
+     */
1260
+    private function getAttributeValuesFromEntry($result, $attribute, &$known) {
1261
+        if (!is_array($result)
1262
+           || !isset($result['count'])
1263
+           || !$result['count'] > 0) {
1264
+            return self::LRESULT_PROCESSED_INVALID;
1265
+        }
1266
+
1267
+        // strtolower on all keys for proper comparison
1268
+        $result = \OCP\Util::mb_array_change_key_case($result);
1269
+        $attribute = strtolower($attribute);
1270
+        if (isset($result[$attribute])) {
1271
+            foreach ($result[$attribute] as $key => $val) {
1272
+                if ($key === 'count') {
1273
+                    continue;
1274
+                }
1275
+                if (!in_array($val, $known)) {
1276
+                    $known[] = $val;
1277
+                }
1278
+            }
1279
+            return self::LRESULT_PROCESSED_OK;
1280
+        } else {
1281
+            return self::LRESULT_PROCESSED_SKIP;
1282
+        }
1283
+    }
1284
+
1285
+    /**
1286
+     * @return bool|mixed
1287
+     */
1288
+    private function getConnection() {
1289
+        if (!is_null($this->cr)) {
1290
+            return $this->cr;
1291
+        }
1292
+
1293
+        $cr = $this->ldap->connect(
1294
+            $this->configuration->ldapHost,
1295
+            $this->configuration->ldapPort
1296
+        );
1297
+
1298
+        $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1299
+        $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1300
+        $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1301
+        if ($this->configuration->ldapTLS === 1) {
1302
+            $this->ldap->startTls($cr);
1303
+        }
1304
+
1305
+        $lo = @$this->ldap->bind($cr,
1306
+                                    $this->configuration->ldapAgentName,
1307
+                                    $this->configuration->ldapAgentPassword);
1308
+        if ($lo === true) {
1309
+            $this->$cr = $cr;
1310
+            return $cr;
1311
+        }
1312
+
1313
+        return false;
1314
+    }
1315
+
1316
+    /**
1317
+     * @return array
1318
+     */
1319
+    private function getDefaultLdapPortSettings() {
1320
+        static $settings = [
1321
+            ['port' => 7636, 'tls' => false],
1322
+            ['port' =>  636, 'tls' => false],
1323
+            ['port' => 7389, 'tls' => true],
1324
+            ['port' =>  389, 'tls' => true],
1325
+            ['port' => 7389, 'tls' => false],
1326
+            ['port' =>  389, 'tls' => false],
1327
+        ];
1328
+        return $settings;
1329
+    }
1330
+
1331
+    /**
1332
+     * @return array
1333
+     */
1334
+    private function getPortSettingsToTry() {
1335
+        //389 ← LDAP / Unencrypted or StartTLS
1336
+        //636 ← LDAPS / SSL
1337
+        //7xxx ← UCS. need to be checked first, because both ports may be open
1338
+        $host = $this->configuration->ldapHost;
1339
+        $port = (int)$this->configuration->ldapPort;
1340
+        $portSettings = [];
1341
+
1342
+        //In case the port is already provided, we will check this first
1343
+        if ($port > 0) {
1344
+            $hostInfo = parse_url($host);
1345
+            if (!(is_array($hostInfo)
1346
+                && isset($hostInfo['scheme'])
1347
+                && stripos($hostInfo['scheme'], 'ldaps') !== false)) {
1348
+                $portSettings[] = ['port' => $port, 'tls' => true];
1349
+            }
1350
+            $portSettings[] =['port' => $port, 'tls' => false];
1351
+        }
1352
+
1353
+        //default ports
1354
+        $portSettings = array_merge($portSettings,
1355
+                                    $this->getDefaultLdapPortSettings());
1356
+
1357
+        return $portSettings;
1358
+    }
1359 1359
 }
Please login to merge, or discard this patch.
apps/user_ldap/templates/settings.php 2 patches
Indentation   +54 added lines, -54 removed lines patch added patch discarded remove patch
@@ -5,46 +5,46 @@  discard block
 block discarded – undo
5 5
 vendor_style('user_ldap', 'ui-multiselect/jquery.multiselect');
6 6
 
7 7
 script('user_ldap', [
8
-	'wizard/controller',
9
-	'wizard/configModel',
10
-	'wizard/view',
11
-	'wizard/wizardObject',
12
-	'wizard/wizardTabGeneric',
13
-	'wizard/wizardTabElementary',
14
-	'wizard/wizardTabAbstractFilter',
15
-	'wizard/wizardTabUserFilter',
16
-	'wizard/wizardTabLoginFilter',
17
-	'wizard/wizardTabGroupFilter',
18
-	'wizard/wizardTabAdvanced',
19
-	'wizard/wizardTabExpert',
20
-	'wizard/wizardDetectorQueue',
21
-	'wizard/wizardDetectorGeneric',
22
-	'wizard/wizardDetectorPort',
23
-	'wizard/wizardDetectorBaseDN',
24
-	'wizard/wizardDetectorFeatureAbstract',
25
-	'wizard/wizardDetectorUserObjectClasses',
26
-	'wizard/wizardDetectorGroupObjectClasses',
27
-	'wizard/wizardDetectorGroupsForUsers',
28
-	'wizard/wizardDetectorGroupsForGroups',
29
-	'wizard/wizardDetectorSimpleRequestAbstract',
30
-	'wizard/wizardDetectorFilterUser',
31
-	'wizard/wizardDetectorFilterLogin',
32
-	'wizard/wizardDetectorFilterGroup',
33
-	'wizard/wizardDetectorUserCount',
34
-	'wizard/wizardDetectorGroupCount',
35
-	'wizard/wizardDetectorEmailAttribute',
36
-	'wizard/wizardDetectorUserDisplayNameAttribute',
37
-	'wizard/wizardDetectorUserGroupAssociation',
38
-	'wizard/wizardDetectorAvailableAttributes',
39
-	'wizard/wizardDetectorTestAbstract',
40
-	'wizard/wizardDetectorTestLoginName',
41
-	'wizard/wizardDetectorTestBaseDN',
42
-	'wizard/wizardDetectorTestConfiguration',
43
-	'wizard/wizardDetectorClearUserMappings',
44
-	'wizard/wizardDetectorClearGroupMappings',
45
-	'wizard/wizardFilterOnType',
46
-	'wizard/wizardFilterOnTypeFactory',
47
-	'wizard/wizard'
8
+    'wizard/controller',
9
+    'wizard/configModel',
10
+    'wizard/view',
11
+    'wizard/wizardObject',
12
+    'wizard/wizardTabGeneric',
13
+    'wizard/wizardTabElementary',
14
+    'wizard/wizardTabAbstractFilter',
15
+    'wizard/wizardTabUserFilter',
16
+    'wizard/wizardTabLoginFilter',
17
+    'wizard/wizardTabGroupFilter',
18
+    'wizard/wizardTabAdvanced',
19
+    'wizard/wizardTabExpert',
20
+    'wizard/wizardDetectorQueue',
21
+    'wizard/wizardDetectorGeneric',
22
+    'wizard/wizardDetectorPort',
23
+    'wizard/wizardDetectorBaseDN',
24
+    'wizard/wizardDetectorFeatureAbstract',
25
+    'wizard/wizardDetectorUserObjectClasses',
26
+    'wizard/wizardDetectorGroupObjectClasses',
27
+    'wizard/wizardDetectorGroupsForUsers',
28
+    'wizard/wizardDetectorGroupsForGroups',
29
+    'wizard/wizardDetectorSimpleRequestAbstract',
30
+    'wizard/wizardDetectorFilterUser',
31
+    'wizard/wizardDetectorFilterLogin',
32
+    'wizard/wizardDetectorFilterGroup',
33
+    'wizard/wizardDetectorUserCount',
34
+    'wizard/wizardDetectorGroupCount',
35
+    'wizard/wizardDetectorEmailAttribute',
36
+    'wizard/wizardDetectorUserDisplayNameAttribute',
37
+    'wizard/wizardDetectorUserGroupAssociation',
38
+    'wizard/wizardDetectorAvailableAttributes',
39
+    'wizard/wizardDetectorTestAbstract',
40
+    'wizard/wizardDetectorTestLoginName',
41
+    'wizard/wizardDetectorTestBaseDN',
42
+    'wizard/wizardDetectorTestConfiguration',
43
+    'wizard/wizardDetectorClearUserMappings',
44
+    'wizard/wizardDetectorClearGroupMappings',
45
+    'wizard/wizardFilterOnType',
46
+    'wizard/wizardFilterOnTypeFactory',
47
+    'wizard/wizard'
48 48
 ]);
49 49
 
50 50
 style('user_ldap', 'settings');
@@ -67,10 +67,10 @@  discard block
 block discarded – undo
67 67
 		<li class="ldapSettingsTabs"><a href="#ldapSettings-1"><?php p($l->t('Advanced'));?></a></li>
68 68
 	</ul>
69 69
 	<?php
70
-	if (!function_exists('ldap_connect')) {
71
-		print_unescaped('<p class="ldapwarning">'.$l->t('<b>Warning:</b> The PHP LDAP module is not installed, the backend will not work. Please ask your system administrator to install it.').'</p>');
72
-	}
73
-	?>
70
+    if (!function_exists('ldap_connect')) {
71
+        print_unescaped('<p class="ldapwarning">'.$l->t('<b>Warning:</b> The PHP LDAP module is not installed, the backend will not work. Please ask your system administrator to install it.').'</p>');
72
+    }
73
+    ?>
74 74
 	<?php require_once __DIR__ . '/part.wizard-server.php'; ?>
75 75
 	<?php require_once __DIR__ . '/part.wizard-userfilter.php'; ?>
76 76
 	<?php require_once __DIR__ . '/part.wizard-loginfilter.php'; ?>
@@ -96,16 +96,16 @@  discard block
 block discarded – undo
96 96
 				<p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree'));?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line'));?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree'));?>"></textarea></p>
97 97
 				<p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes'));?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes'));?>"></textarea></p>
98 98
 				<p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association'));?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) {
99
-		p(' selected');
100
-	} ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) {
101
-		p(' selected');
102
-	} ?>>memberUid</option><option value="member"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'member')) {
103
-		p(' selected');
104
-	} ?>>member (AD)</option><option value="gidNumber"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'gidNumber')) {
105
-		p(' selected');
106
-	} ?>>gidNumber</option><option value="zimbraMailForwardingAddress"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'zimbraMailForwardingAddress')) {
107
-		p(' selected');
108
-	} ?>>zimbraMailForwardingAddress</option></select></p>
99
+        p(' selected');
100
+    } ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) {
101
+        p(' selected');
102
+    } ?>>memberUid</option><option value="member"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'member')) {
103
+        p(' selected');
104
+    } ?>>member (AD)</option><option value="gidNumber"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'gidNumber')) {
105
+        p(' selected');
106
+    } ?>>gidNumber</option><option value="zimbraMailForwardingAddress"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'zimbraMailForwardingAddress')) {
107
+        p(' selected');
108
+    } ?>>zimbraMailForwardingAddress</option></select></p>
109 109
 				<p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p>
110 110
 				<p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups'));?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>"  title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)'));?>" /></p>
111 111
 				<p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize'));?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)'));?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p>
Please login to merge, or discard this patch.
Spacing   +46 added lines, -46 removed lines patch added patch discarded remove patch
@@ -59,43 +59,43 @@  discard block
 block discarded – undo
59 59
 
60 60
 	<div id="ldapSettings">
61 61
 	<ul>
62
-		<li id="#ldapWizard1"><a href="#ldapWizard1"><?php p($l->t('Server'));?></a></li>
63
-		<li id="#ldapWizard2"><a href="#ldapWizard2"><?php p($l->t('Users'));?></a></li>
64
-		<li id="#ldapWizard3"><a href="#ldapWizard3"><?php p($l->t('Login Attributes'));?></a></li>
65
-		<li id="#ldapWizard4"><a href="#ldapWizard4"><?php p($l->t('Groups'));?></a></li>
66
-		<li class="ldapSettingsTabs"><a href="#ldapSettings-2"><?php p($l->t('Expert'));?></a></li>
67
-		<li class="ldapSettingsTabs"><a href="#ldapSettings-1"><?php p($l->t('Advanced'));?></a></li>
62
+		<li id="#ldapWizard1"><a href="#ldapWizard1"><?php p($l->t('Server')); ?></a></li>
63
+		<li id="#ldapWizard2"><a href="#ldapWizard2"><?php p($l->t('Users')); ?></a></li>
64
+		<li id="#ldapWizard3"><a href="#ldapWizard3"><?php p($l->t('Login Attributes')); ?></a></li>
65
+		<li id="#ldapWizard4"><a href="#ldapWizard4"><?php p($l->t('Groups')); ?></a></li>
66
+		<li class="ldapSettingsTabs"><a href="#ldapSettings-2"><?php p($l->t('Expert')); ?></a></li>
67
+		<li class="ldapSettingsTabs"><a href="#ldapSettings-1"><?php p($l->t('Advanced')); ?></a></li>
68 68
 	</ul>
69 69
 	<?php
70 70
 	if (!function_exists('ldap_connect')) {
71 71
 		print_unescaped('<p class="ldapwarning">'.$l->t('<b>Warning:</b> The PHP LDAP module is not installed, the backend will not work. Please ask your system administrator to install it.').'</p>');
72 72
 	}
73 73
 	?>
74
-	<?php require_once __DIR__ . '/part.wizard-server.php'; ?>
75
-	<?php require_once __DIR__ . '/part.wizard-userfilter.php'; ?>
76
-	<?php require_once __DIR__ . '/part.wizard-loginfilter.php'; ?>
77
-	<?php require_once __DIR__ . '/part.wizard-groupfilter.php'; ?>
74
+	<?php require_once __DIR__.'/part.wizard-server.php'; ?>
75
+	<?php require_once __DIR__.'/part.wizard-userfilter.php'; ?>
76
+	<?php require_once __DIR__.'/part.wizard-loginfilter.php'; ?>
77
+	<?php require_once __DIR__.'/part.wizard-groupfilter.php'; ?>
78 78
 	<fieldset id="ldapSettings-1">
79 79
 		<div id="ldapAdvancedAccordion">
80
-			<h3><?php p($l->t('Connection Settings'));?></h3>
80
+			<h3><?php p($l->t('Connection Settings')); ?></h3>
81 81
 			<div>
82
-				<p><label for="ldap_configuration_active"><?php p($l->t('Configuration Active'));?></label><input type="checkbox" id="ldap_configuration_active" name="ldap_configuration_active" value="1" data-default="<?php p($_['ldap_configuration_active_default']); ?>"  title="<?php p($l->t('When unchecked, this configuration will be skipped.'));?>" /></p>
83
-				<p><label for="ldap_backup_host"><?php p($l->t('Backup (Replica) Host'));?></label><input type="text" id="ldap_backup_host" name="ldap_backup_host" data-default="<?php p($_['ldap_backup_host_default']); ?>" title="<?php p($l->t('Give an optional backup host. It must be a replica of the main LDAP/AD server.'));?>"></p>
84
-				<p><label for="ldap_backup_port"><?php p($l->t('Backup (Replica) Port'));?></label><input type="number" id="ldap_backup_port" name="ldap_backup_port" data-default="<?php p($_['ldap_backup_port_default']); ?>"  /></p>
85
-				<p><label for="ldap_override_main_server"><?php p($l->t('Disable Main Server'));?></label><input type="checkbox" id="ldap_override_main_server" name="ldap_override_main_server" value="1" data-default="<?php p($_['ldap_override_main_server_default']); ?>"  title="<?php p($l->t('Only connect to the replica server.'));?>" /></p>
86
-				<p><label for="ldap_turn_off_cert_check"><?php p($l->t('Turn off SSL certificate validation.'));?></label><input type="checkbox" id="ldap_turn_off_cert_check" name="ldap_turn_off_cert_check" title="<?php p($l->t('Not recommended, use it for testing only! If connection only works with this option, import the LDAP server\'s SSL certificate in your %s server.', [$theme->getName()]));?>" data-default="<?php p($_['ldap_turn_off_cert_check_default']); ?>" value="1"><br/></p>
87
-				<p><label for="ldap_cache_ttl"><?php p($l->t('Cache Time-To-Live'));?></label><input type="number" id="ldap_cache_ttl" name="ldap_cache_ttl" title="<?php p($l->t('in seconds. A change empties the cache.'));?>" data-default="<?php p($_['ldap_cache_ttl_default']); ?>" /></p>
82
+				<p><label for="ldap_configuration_active"><?php p($l->t('Configuration Active')); ?></label><input type="checkbox" id="ldap_configuration_active" name="ldap_configuration_active" value="1" data-default="<?php p($_['ldap_configuration_active_default']); ?>"  title="<?php p($l->t('When unchecked, this configuration will be skipped.')); ?>" /></p>
83
+				<p><label for="ldap_backup_host"><?php p($l->t('Backup (Replica) Host')); ?></label><input type="text" id="ldap_backup_host" name="ldap_backup_host" data-default="<?php p($_['ldap_backup_host_default']); ?>" title="<?php p($l->t('Give an optional backup host. It must be a replica of the main LDAP/AD server.')); ?>"></p>
84
+				<p><label for="ldap_backup_port"><?php p($l->t('Backup (Replica) Port')); ?></label><input type="number" id="ldap_backup_port" name="ldap_backup_port" data-default="<?php p($_['ldap_backup_port_default']); ?>"  /></p>
85
+				<p><label for="ldap_override_main_server"><?php p($l->t('Disable Main Server')); ?></label><input type="checkbox" id="ldap_override_main_server" name="ldap_override_main_server" value="1" data-default="<?php p($_['ldap_override_main_server_default']); ?>"  title="<?php p($l->t('Only connect to the replica server.')); ?>" /></p>
86
+				<p><label for="ldap_turn_off_cert_check"><?php p($l->t('Turn off SSL certificate validation.')); ?></label><input type="checkbox" id="ldap_turn_off_cert_check" name="ldap_turn_off_cert_check" title="<?php p($l->t('Not recommended, use it for testing only! If connection only works with this option, import the LDAP server\'s SSL certificate in your %s server.', [$theme->getName()])); ?>" data-default="<?php p($_['ldap_turn_off_cert_check_default']); ?>" value="1"><br/></p>
87
+				<p><label for="ldap_cache_ttl"><?php p($l->t('Cache Time-To-Live')); ?></label><input type="number" id="ldap_cache_ttl" name="ldap_cache_ttl" title="<?php p($l->t('in seconds. A change empties the cache.')); ?>" data-default="<?php p($_['ldap_cache_ttl_default']); ?>" /></p>
88 88
 			</div>
89
-			<h3><?php p($l->t('Directory Settings'));?></h3>
89
+			<h3><?php p($l->t('Directory Settings')); ?></h3>
90 90
 			<div>
91
-				<p><label for="ldap_display_name"><?php p($l->t('User Display Name Field'));?></label><input type="text" id="ldap_display_name" name="ldap_display_name" data-default="<?php p($_['ldap_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the user\'s display name.'));?>" /></p>
92
-				<p><label for="ldap_user_display_name_2"><?php p($l->t('2nd User Display Name Field'));?></label><input type="text" id="ldap_user_display_name_2" name="ldap_user_display_name_2" data-default="<?php p($_['ldap_user_display_name_2_default']); ?>" title="<?php p($l->t('Optional. An LDAP attribute to be added to the display name in brackets. Results in e.g. »John Doe ([email protected])«.'));?>" /></p>
93
-				<p><label for="ldap_base_users"><?php p($l->t('Base User Tree'));?></label><textarea id="ldap_base_users" name="ldap_base_users" placeholder="<?php p($l->t('One User Base DN per line'));?>" data-default="<?php p($_['ldap_base_users_default']); ?>" title="<?php p($l->t('Base User Tree'));?>"></textarea></p>
94
-				<p><label for="ldap_attributes_for_user_search"><?php p($l->t('User Search Attributes'));?></label><textarea id="ldap_attributes_for_user_search" name="ldap_attributes_for_user_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_user_search_default']); ?>" title="<?php p($l->t('User Search Attributes'));?>"></textarea></p>
95
-				<p><label for="ldap_group_display_name"><?php p($l->t('Group Display Name Field'));?></label><input type="text" id="ldap_group_display_name" name="ldap_group_display_name" data-default="<?php p($_['ldap_group_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the groups\'s display name.'));?>" /></p>
96
-				<p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree'));?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line'));?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree'));?>"></textarea></p>
97
-				<p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes'));?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes'));?>"></textarea></p>
98
-				<p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association'));?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) {
91
+				<p><label for="ldap_display_name"><?php p($l->t('User Display Name Field')); ?></label><input type="text" id="ldap_display_name" name="ldap_display_name" data-default="<?php p($_['ldap_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the user\'s display name.')); ?>" /></p>
92
+				<p><label for="ldap_user_display_name_2"><?php p($l->t('2nd User Display Name Field')); ?></label><input type="text" id="ldap_user_display_name_2" name="ldap_user_display_name_2" data-default="<?php p($_['ldap_user_display_name_2_default']); ?>" title="<?php p($l->t('Optional. An LDAP attribute to be added to the display name in brackets. Results in e.g. »John Doe ([email protected])«.')); ?>" /></p>
93
+				<p><label for="ldap_base_users"><?php p($l->t('Base User Tree')); ?></label><textarea id="ldap_base_users" name="ldap_base_users" placeholder="<?php p($l->t('One User Base DN per line')); ?>" data-default="<?php p($_['ldap_base_users_default']); ?>" title="<?php p($l->t('Base User Tree')); ?>"></textarea></p>
94
+				<p><label for="ldap_attributes_for_user_search"><?php p($l->t('User Search Attributes')); ?></label><textarea id="ldap_attributes_for_user_search" name="ldap_attributes_for_user_search" placeholder="<?php p($l->t('Optional; one attribute per line')); ?>" data-default="<?php p($_['ldap_attributes_for_user_search_default']); ?>" title="<?php p($l->t('User Search Attributes')); ?>"></textarea></p>
95
+				<p><label for="ldap_group_display_name"><?php p($l->t('Group Display Name Field')); ?></label><input type="text" id="ldap_group_display_name" name="ldap_group_display_name" data-default="<?php p($_['ldap_group_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the groups\'s display name.')); ?>" /></p>
96
+				<p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree')); ?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line')); ?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree')); ?>"></textarea></p>
97
+				<p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes')); ?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line')); ?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes')); ?>"></textarea></p>
98
+				<p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association')); ?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) {
99 99
 		p(' selected');
100 100
 	} ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) {
101 101
 		p(' selected');
@@ -106,35 +106,35 @@  discard block
 block discarded – undo
106 106
 	} ?>>gidNumber</option><option value="zimbraMailForwardingAddress"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'zimbraMailForwardingAddress')) {
107 107
 		p(' selected');
108 108
 	} ?>>zimbraMailForwardingAddress</option></select></p>
109
-				<p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p>
110
-				<p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups'));?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>"  title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)'));?>" /></p>
111
-				<p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize'));?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)'));?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p>
112
-				<p><label for="ldap_turn_on_pwd_change"><?php p($l->t('Enable LDAP password changes per user'));?></label><span class="inlinetable"><span class="tablerow left"><input type="checkbox" id="ldap_turn_on_pwd_change" name="ldap_turn_on_pwd_change" value="1" data-default="<?php p($_['ldap_turn_on_pwd_change_default']); ?>" title="<?php p($l->t('Allow LDAP users to change their password and allow Super Administrators and Group Administrators to change the password of their LDAP users. Only works when access control policies are configured accordingly on the LDAP server. As passwords are sent in plaintext to the LDAP server, transport encryption must be used and password hashing should be configured on the LDAP server.'));?>" /><span class="tablecell"><?php p($l->t('(New password is sent as plain text to LDAP)'));?></span></span>
109
+				<p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL')); ?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)')); ?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p>
110
+				<p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups')); ?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>"  title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)')); ?>" /></p>
111
+				<p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize')); ?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)')); ?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p>
112
+				<p><label for="ldap_turn_on_pwd_change"><?php p($l->t('Enable LDAP password changes per user')); ?></label><span class="inlinetable"><span class="tablerow left"><input type="checkbox" id="ldap_turn_on_pwd_change" name="ldap_turn_on_pwd_change" value="1" data-default="<?php p($_['ldap_turn_on_pwd_change_default']); ?>" title="<?php p($l->t('Allow LDAP users to change their password and allow Super Administrators and Group Administrators to change the password of their LDAP users. Only works when access control policies are configured accordingly on the LDAP server. As passwords are sent in plaintext to the LDAP server, transport encryption must be used and password hashing should be configured on the LDAP server.')); ?>" /><span class="tablecell"><?php p($l->t('(New password is sent as plain text to LDAP)')); ?></span></span>
113 113
 			</span><br/></p>
114
-				<p><label for="ldap_default_ppolicy_dn"><?php p($l->t('Default password policy DN'));?></label><input type="text" id="ldap_default_ppolicy_dn" name="ldap_default_ppolicy_dn" title="<?php p($l->t('The DN of a default password policy that will be used for password expiry handling. Works only when LDAP password changes per user are enabled and is only supported by OpenLDAP. Leave empty to disable password expiry handling.'));?>" data-default="<?php p($_['ldap_default_ppolicy_dn_default']); ?>" /></p>
114
+				<p><label for="ldap_default_ppolicy_dn"><?php p($l->t('Default password policy DN')); ?></label><input type="text" id="ldap_default_ppolicy_dn" name="ldap_default_ppolicy_dn" title="<?php p($l->t('The DN of a default password policy that will be used for password expiry handling. Works only when LDAP password changes per user are enabled and is only supported by OpenLDAP. Leave empty to disable password expiry handling.')); ?>" data-default="<?php p($_['ldap_default_ppolicy_dn_default']); ?>" /></p>
115 115
 			</div>
116
-			<h3><?php p($l->t('Special Attributes'));?></h3>
116
+			<h3><?php p($l->t('Special Attributes')); ?></h3>
117 117
 			<div>
118
-				<p><label for="ldap_quota_attr"><?php p($l->t('Quota Field'));?></label><input type="text" id="ldap_quota_attr" name="ldap_quota_attr" data-default="<?php p($_['ldap_quota_attr_default']); ?>" title="<?php p($l->t('Leave empty for user\'s default quota. Otherwise, specify an LDAP/AD attribute.'));?>" /></p>
119
-				<p><label for="ldap_quota_def"><?php p($l->t('Quota Default'));?></label><input type="text" id="ldap_quota_def" name="ldap_quota_def" data-default="<?php p($_['ldap_quota_def_default']); ?>" title="<?php p($l->t('Override default quota for LDAP users who do not have a quota set in the Quota Field.'));?>" /></p>
120
-				<p><label for="ldap_email_attr"><?php p($l->t('Email Field'));?></label><input type="text" id="ldap_email_attr" name="ldap_email_attr" data-default="<?php p($_['ldap_email_attr_default']); ?>" title="<?php p($l->t('Set the user\'s email from their LDAP attribute. Leave it empty for default behaviour.'));?>" /></p>
121
-				<p><label for="home_folder_naming_rule"><?php p($l->t('User Home Folder Naming Rule'));?></label><input type="text" id="home_folder_naming_rule" name="home_folder_naming_rule" title="<?php p($l->t('Leave empty for username (default). Otherwise, specify an LDAP/AD attribute.'));?>" data-default="<?php p($_['home_folder_naming_rule_default']); ?>" /></p>
118
+				<p><label for="ldap_quota_attr"><?php p($l->t('Quota Field')); ?></label><input type="text" id="ldap_quota_attr" name="ldap_quota_attr" data-default="<?php p($_['ldap_quota_attr_default']); ?>" title="<?php p($l->t('Leave empty for user\'s default quota. Otherwise, specify an LDAP/AD attribute.')); ?>" /></p>
119
+				<p><label for="ldap_quota_def"><?php p($l->t('Quota Default')); ?></label><input type="text" id="ldap_quota_def" name="ldap_quota_def" data-default="<?php p($_['ldap_quota_def_default']); ?>" title="<?php p($l->t('Override default quota for LDAP users who do not have a quota set in the Quota Field.')); ?>" /></p>
120
+				<p><label for="ldap_email_attr"><?php p($l->t('Email Field')); ?></label><input type="text" id="ldap_email_attr" name="ldap_email_attr" data-default="<?php p($_['ldap_email_attr_default']); ?>" title="<?php p($l->t('Set the user\'s email from their LDAP attribute. Leave it empty for default behaviour.')); ?>" /></p>
121
+				<p><label for="home_folder_naming_rule"><?php p($l->t('User Home Folder Naming Rule')); ?></label><input type="text" id="home_folder_naming_rule" name="home_folder_naming_rule" title="<?php p($l->t('Leave empty for username (default). Otherwise, specify an LDAP/AD attribute.')); ?>" data-default="<?php p($_['home_folder_naming_rule_default']); ?>" /></p>
122 122
 				<p><label for="ldap_ext_storage_home_attribute"> <?php p($l->t('"$home" Placeholder Field')); ?></label><input type="text" id="ldap_ext_storage_home_attribute" name="ldap_ext_storage_home_attribute" title="<?php p($l->t('$home in an external storage configuration will be replaced with the value of the specified attribute')); ?>" data-default="<?php p($_['ldap_ext_storage_home_attribute_default']); ?>"></p>
123 123
 			</div>
124 124
 		</div>
125 125
 		<?php print_unescaped($_['settingControls']); ?>
126 126
 	</fieldset>
127 127
 	<fieldset id="ldapSettings-2">
128
-		<p><strong><?php p($l->t('Internal Username'));?></strong></p>
129
-		<p class="ldapIndent"><?php p($l->t('By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [ a-zA-Z0-9_.@- ].  Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all *DAV services. With this setting, the default behavior can be overridden. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users.'));?></p>
130
-		<p class="ldapIndent"><label for="ldap_expert_username_attr"><?php p($l->t('Internal Username Attribute:'));?></label><input type="text" id="ldap_expert_username_attr" name="ldap_expert_username_attr" data-default="<?php p($_['ldap_expert_username_attr_default']); ?>" /></p>
131
-		<p><strong><?php p($l->t('Override UUID detection'));?></strong></p>
132
-		<p class="ldapIndent"><?php p($l->t('By default, the UUID attribute is automatically detected. The UUID attribute is used to doubtlessly identify LDAP users and groups. Also, the internal username will be created based on the UUID, if not specified otherwise above. You can override the setting and pass an attribute of your choice. You must make sure that the attribute of your choice can be fetched for both users and groups and it is unique. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users and groups.'));?></p>
133
-		<p class="ldapIndent"><label for="ldap_expert_uuid_user_attr"><?php p($l->t('UUID Attribute for Users:'));?></label><input type="text" id="ldap_expert_uuid_user_attr" name="ldap_expert_uuid_user_attr" data-default="<?php p($_['ldap_expert_uuid_user_attr_default']); ?>" /></p>
134
-		<p class="ldapIndent"><label for="ldap_expert_uuid_group_attr"><?php p($l->t('UUID Attribute for Groups:'));?></label><input type="text" id="ldap_expert_uuid_group_attr" name="ldap_expert_uuid_group_attr" data-default="<?php p($_['ldap_expert_uuid_group_attr_default']); ?>" /></p>
135
-		<p><strong><?php p($l->t('Username-LDAP User Mapping'));?></strong></p>
136
-		<p class="ldapIndent"><?php p($l->t('Usernames are used to store and assign metadata. In order to precisely identify and recognize users, each LDAP user will have an internal username. This requires a mapping from username to LDAP user. The created username is mapped to the UUID of the LDAP user. Additionally the DN is cached as well to reduce LDAP interaction, but it is not used for identification. If the DN changes, the changes will be found. The internal username is used all over. Clearing the mappings will have leftovers everywhere. Clearing the mappings is not configuration sensitive, it affects all LDAP configurations! Never clear the mappings in a production environment, only in a testing or experimental stage.'));?></p>
137
-		<p class="ldapIndent"><button type="button" id="ldap_action_clear_user_mappings" name="ldap_action_clear_user_mappings"><?php p($l->t('Clear Username-LDAP User Mapping'));?></button><br/><button type="button" id="ldap_action_clear_group_mappings" name="ldap_action_clear_group_mappings"><?php p($l->t('Clear Groupname-LDAP Group Mapping'));?></button></p>
128
+		<p><strong><?php p($l->t('Internal Username')); ?></strong></p>
129
+		<p class="ldapIndent"><?php p($l->t('By default the internal username will be created from the UUID attribute. It makes sure that the username is unique and characters do not need to be converted. The internal username has the restriction that only these characters are allowed: [ a-zA-Z0-9_.@- ].  Other characters are replaced with their ASCII correspondence or simply omitted. On collisions a number will be added/increased. The internal username is used to identify a user internally. It is also the default name for the user home folder. It is also a part of remote URLs, for instance for all *DAV services. With this setting, the default behavior can be overridden. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users.')); ?></p>
130
+		<p class="ldapIndent"><label for="ldap_expert_username_attr"><?php p($l->t('Internal Username Attribute:')); ?></label><input type="text" id="ldap_expert_username_attr" name="ldap_expert_username_attr" data-default="<?php p($_['ldap_expert_username_attr_default']); ?>" /></p>
131
+		<p><strong><?php p($l->t('Override UUID detection')); ?></strong></p>
132
+		<p class="ldapIndent"><?php p($l->t('By default, the UUID attribute is automatically detected. The UUID attribute is used to doubtlessly identify LDAP users and groups. Also, the internal username will be created based on the UUID, if not specified otherwise above. You can override the setting and pass an attribute of your choice. You must make sure that the attribute of your choice can be fetched for both users and groups and it is unique. Leave it empty for default behavior. Changes will have effect only on newly mapped (added) LDAP users and groups.')); ?></p>
133
+		<p class="ldapIndent"><label for="ldap_expert_uuid_user_attr"><?php p($l->t('UUID Attribute for Users:')); ?></label><input type="text" id="ldap_expert_uuid_user_attr" name="ldap_expert_uuid_user_attr" data-default="<?php p($_['ldap_expert_uuid_user_attr_default']); ?>" /></p>
134
+		<p class="ldapIndent"><label for="ldap_expert_uuid_group_attr"><?php p($l->t('UUID Attribute for Groups:')); ?></label><input type="text" id="ldap_expert_uuid_group_attr" name="ldap_expert_uuid_group_attr" data-default="<?php p($_['ldap_expert_uuid_group_attr_default']); ?>" /></p>
135
+		<p><strong><?php p($l->t('Username-LDAP User Mapping')); ?></strong></p>
136
+		<p class="ldapIndent"><?php p($l->t('Usernames are used to store and assign metadata. In order to precisely identify and recognize users, each LDAP user will have an internal username. This requires a mapping from username to LDAP user. The created username is mapped to the UUID of the LDAP user. Additionally the DN is cached as well to reduce LDAP interaction, but it is not used for identification. If the DN changes, the changes will be found. The internal username is used all over. Clearing the mappings will have leftovers everywhere. Clearing the mappings is not configuration sensitive, it affects all LDAP configurations! Never clear the mappings in a production environment, only in a testing or experimental stage.')); ?></p>
137
+		<p class="ldapIndent"><button type="button" id="ldap_action_clear_user_mappings" name="ldap_action_clear_user_mappings"><?php p($l->t('Clear Username-LDAP User Mapping')); ?></button><br/><button type="button" id="ldap_action_clear_group_mappings" name="ldap_action_clear_group_mappings"><?php p($l->t('Clear Groupname-LDAP Group Mapping')); ?></button></p>
138 138
 		<?php print_unescaped($_['settingControls']); ?>
139 139
 	</fieldset>
140 140
 	</div>
Please login to merge, or discard this patch.