Passed
Push — master ( 326a04...579c70 )
by Blizzz
12:55 queued 11s
created
apps/user_ldap/lib/Group_LDAP.php 1 patch
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.
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 1 patch
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.