Passed
Push — master ( db782f...d72d9f )
by Morris
32:30 queued 18:44
created
apps/user_ldap/lib/Access.php 1 patch
Indentation   +1984 added lines, -1984 removed lines patch added patch discarded remove patch
@@ -63,1771 +63,1771 @@  discard block
 block discarded – undo
63 63
  * @package OCA\User_LDAP
64 64
  */
65 65
 class Access extends LDAPUtility {
66
-	public const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
67
-
68
-	/** @var \OCA\User_LDAP\Connection */
69
-	public $connection;
70
-	/** @var Manager */
71
-	public $userManager;
72
-	//never ever check this var directly, always use getPagedSearchResultState
73
-	protected $pagedSearchedSuccessful;
74
-
75
-	/**
76
-	 * protected $cookies = [];
77
-	 *
78
-	 * @var AbstractMapping $userMapper
79
-	 */
80
-	protected $userMapper;
81
-
82
-	/**
83
-	 * @var AbstractMapping $userMapper
84
-	 */
85
-	protected $groupMapper;
86
-
87
-	/**
88
-	 * @var \OCA\User_LDAP\Helper
89
-	 */
90
-	private $helper;
91
-	/** @var IConfig */
92
-	private $config;
93
-	/** @var IUserManager */
94
-	private $ncUserManager;
95
-	/** @var string */
96
-	private $lastCookie = '';
97
-
98
-	public function __construct(
99
-		Connection $connection,
100
-		ILDAPWrapper $ldap,
101
-		Manager $userManager,
102
-		Helper $helper,
103
-		IConfig $config,
104
-		IUserManager $ncUserManager
105
-	) {
106
-		parent::__construct($ldap);
107
-		$this->connection = $connection;
108
-		$this->userManager = $userManager;
109
-		$this->userManager->setLdapAccess($this);
110
-		$this->helper = $helper;
111
-		$this->config = $config;
112
-		$this->ncUserManager = $ncUserManager;
113
-	}
114
-
115
-	/**
116
-	 * sets the User Mapper
117
-	 *
118
-	 * @param AbstractMapping $mapper
119
-	 */
120
-	public function setUserMapper(AbstractMapping $mapper) {
121
-		$this->userMapper = $mapper;
122
-	}
123
-
124
-	/**
125
-	 * returns the User Mapper
126
-	 *
127
-	 * @return AbstractMapping
128
-	 * @throws \Exception
129
-	 */
130
-	public function getUserMapper() {
131
-		if (is_null($this->userMapper)) {
132
-			throw new \Exception('UserMapper was not assigned to this Access instance.');
133
-		}
134
-		return $this->userMapper;
135
-	}
136
-
137
-	/**
138
-	 * sets the Group Mapper
139
-	 *
140
-	 * @param AbstractMapping $mapper
141
-	 */
142
-	public function setGroupMapper(AbstractMapping $mapper) {
143
-		$this->groupMapper = $mapper;
144
-	}
145
-
146
-	/**
147
-	 * returns the Group Mapper
148
-	 *
149
-	 * @return AbstractMapping
150
-	 * @throws \Exception
151
-	 */
152
-	public function getGroupMapper() {
153
-		if (is_null($this->groupMapper)) {
154
-			throw new \Exception('GroupMapper was not assigned to this Access instance.');
155
-		}
156
-		return $this->groupMapper;
157
-	}
158
-
159
-	/**
160
-	 * @return bool
161
-	 */
162
-	private function checkConnection() {
163
-		return ($this->connection instanceof Connection);
164
-	}
165
-
166
-	/**
167
-	 * returns the Connection instance
168
-	 *
169
-	 * @return \OCA\User_LDAP\Connection
170
-	 */
171
-	public function getConnection() {
172
-		return $this->connection;
173
-	}
174
-
175
-	/**
176
-	 * reads a given attribute for an LDAP record identified by a DN
177
-	 *
178
-	 * @param string $dn the record in question
179
-	 * @param string $attr the attribute that shall be retrieved
180
-	 *        if empty, just check the record's existence
181
-	 * @param string $filter
182
-	 * @return array|false an array of values on success or an empty
183
-	 *          array if $attr is empty, false otherwise
184
-	 * @throws ServerNotAvailableException
185
-	 */
186
-	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
187
-		if (!$this->checkConnection()) {
188
-			\OCP\Util::writeLog('user_ldap',
189
-				'No LDAP Connector assigned, access impossible for readAttribute.',
190
-				ILogger::WARN);
191
-			return false;
192
-		}
193
-		$cr = $this->connection->getConnectionResource();
194
-		if (!$this->ldap->isResource($cr)) {
195
-			//LDAP not available
196
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
197
-			return false;
198
-		}
199
-		//Cancel possibly running Paged Results operation, otherwise we run in
200
-		//LDAP protocol errors
201
-		$this->abandonPagedSearch();
202
-		// openLDAP requires that we init a new Paged Search. Not needed by AD,
203
-		// but does not hurt either.
204
-		$pagingSize = (int)$this->connection->ldapPagingSize;
205
-		// 0 won't result in replies, small numbers may leave out groups
206
-		// (cf. #12306), 500 is default for paging and should work everywhere.
207
-		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
208
-		$attr = mb_strtolower($attr, 'UTF-8');
209
-		// the actual read attribute later may contain parameters on a ranged
210
-		// request, e.g. member;range=99-199. Depends on server reply.
211
-		$attrToRead = $attr;
212
-
213
-		$values = [];
214
-		$isRangeRequest = false;
215
-		do {
216
-			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
217
-			if (is_bool($result)) {
218
-				// when an exists request was run and it was successful, an empty
219
-				// array must be returned
220
-				return $result ? [] : false;
221
-			}
222
-
223
-			if (!$isRangeRequest) {
224
-				$values = $this->extractAttributeValuesFromResult($result, $attr);
225
-				if (!empty($values)) {
226
-					return $values;
227
-				}
228
-			}
229
-
230
-			$isRangeRequest = false;
231
-			$result = $this->extractRangeData($result, $attr);
232
-			if (!empty($result)) {
233
-				$normalizedResult = $this->extractAttributeValuesFromResult(
234
-					[$attr => $result['values']],
235
-					$attr
236
-				);
237
-				$values = array_merge($values, $normalizedResult);
238
-
239
-				if ($result['rangeHigh'] === '*') {
240
-					// when server replies with * as high range value, there are
241
-					// no more results left
242
-					return $values;
243
-				} else {
244
-					$low = $result['rangeHigh'] + 1;
245
-					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
246
-					$isRangeRequest = true;
247
-				}
248
-			}
249
-		} while ($isRangeRequest);
250
-
251
-		\OCP\Util::writeLog('user_ldap', 'Requested attribute ' . $attr . ' not found for ' . $dn, ILogger::DEBUG);
252
-		return false;
253
-	}
254
-
255
-	/**
256
-	 * Runs an read operation against LDAP
257
-	 *
258
-	 * @param resource $cr the LDAP connection
259
-	 * @param string $dn
260
-	 * @param string $attribute
261
-	 * @param string $filter
262
-	 * @param int $maxResults
263
-	 * @return array|bool false if there was any error, true if an exists check
264
-	 *                    was performed and the requested DN found, array with the
265
-	 *                    returned data on a successful usual operation
266
-	 * @throws ServerNotAvailableException
267
-	 */
268
-	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
269
-		$this->initPagedSearch($filter, $dn, [$attribute], $maxResults, 0);
270
-		$dn = $this->helper->DNasBaseParameter($dn);
271
-		$rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, [$attribute]);
272
-		if (!$this->ldap->isResource($rr)) {
273
-			if ($attribute !== '') {
274
-				//do not throw this message on userExists check, irritates
275
-				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
276
-			}
277
-			//in case an error occurs , e.g. object does not exist
278
-			return false;
279
-		}
280
-		if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
281
-			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
282
-			return true;
283
-		}
284
-		$er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
285
-		if (!$this->ldap->isResource($er)) {
286
-			//did not match the filter, return false
287
-			return false;
288
-		}
289
-		//LDAP attributes are not case sensitive
290
-		$result = \OCP\Util::mb_array_change_key_case(
291
-			$this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
292
-
293
-		return $result;
294
-	}
295
-
296
-	/**
297
-	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
298
-	 * data if present.
299
-	 *
300
-	 * @param array $result from ILDAPWrapper::getAttributes()
301
-	 * @param string $attribute the attribute name that was read
302
-	 * @return string[]
303
-	 */
304
-	public function extractAttributeValuesFromResult($result, $attribute) {
305
-		$values = [];
306
-		if (isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
307
-			$lowercaseAttribute = strtolower($attribute);
308
-			for ($i = 0; $i < $result[$attribute]['count']; $i++) {
309
-				if ($this->resemblesDN($attribute)) {
310
-					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
311
-				} elseif ($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
312
-					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
313
-				} else {
314
-					$values[] = $result[$attribute][$i];
315
-				}
316
-			}
317
-		}
318
-		return $values;
319
-	}
320
-
321
-	/**
322
-	 * Attempts to find ranged data in a getAttribute results and extracts the
323
-	 * returned values as well as information on the range and full attribute
324
-	 * name for further processing.
325
-	 *
326
-	 * @param array $result from ILDAPWrapper::getAttributes()
327
-	 * @param string $attribute the attribute name that was read. Without ";range=…"
328
-	 * @return array If a range was detected with keys 'values', 'attributeName',
329
-	 *               'attributeFull' and 'rangeHigh', otherwise empty.
330
-	 */
331
-	public function extractRangeData($result, $attribute) {
332
-		$keys = array_keys($result);
333
-		foreach ($keys as $key) {
334
-			if ($key !== $attribute && strpos($key, $attribute) === 0) {
335
-				$queryData = explode(';', $key);
336
-				if (strpos($queryData[1], 'range=') === 0) {
337
-					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
338
-					$data = [
339
-						'values' => $result[$key],
340
-						'attributeName' => $queryData[0],
341
-						'attributeFull' => $key,
342
-						'rangeHigh' => $high,
343
-					];
344
-					return $data;
345
-				}
346
-			}
347
-		}
348
-		return [];
349
-	}
350
-
351
-	/**
352
-	 * Set password for an LDAP user identified by a DN
353
-	 *
354
-	 * @param string $userDN the user in question
355
-	 * @param string $password the new password
356
-	 * @return bool
357
-	 * @throws HintException
358
-	 * @throws \Exception
359
-	 */
360
-	public function setPassword($userDN, $password) {
361
-		if ((int)$this->connection->turnOnPasswordChange !== 1) {
362
-			throw new \Exception('LDAP password changes are disabled.');
363
-		}
364
-		$cr = $this->connection->getConnectionResource();
365
-		if (!$this->ldap->isResource($cr)) {
366
-			//LDAP not available
367
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
368
-			return false;
369
-		}
370
-		try {
371
-			// try PASSWD extended operation first
372
-			return @$this->invokeLDAPMethod('exopPasswd', $cr, $userDN, '', $password) ||
373
-				@$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
374
-		} catch (ConstraintViolationException $e) {
375
-			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ') . $e->getMessage(), $e->getCode());
376
-		}
377
-	}
378
-
379
-	/**
380
-	 * checks whether the given attributes value is probably a DN
381
-	 *
382
-	 * @param string $attr the attribute in question
383
-	 * @return boolean if so true, otherwise false
384
-	 */
385
-	private function resemblesDN($attr) {
386
-		$resemblingAttributes = [
387
-			'dn',
388
-			'uniquemember',
389
-			'member',
390
-			// memberOf is an "operational" attribute, without a definition in any RFC
391
-			'memberof'
392
-		];
393
-		return in_array($attr, $resemblingAttributes);
394
-	}
395
-
396
-	/**
397
-	 * checks whether the given string is probably a DN
398
-	 *
399
-	 * @param string $string
400
-	 * @return boolean
401
-	 */
402
-	public function stringResemblesDN($string) {
403
-		$r = $this->ldap->explodeDN($string, 0);
404
-		// if exploding a DN succeeds and does not end up in
405
-		// an empty array except for $r[count] being 0.
406
-		return (is_array($r) && count($r) > 1);
407
-	}
408
-
409
-	/**
410
-	 * returns a DN-string that is cleaned from not domain parts, e.g.
411
-	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
412
-	 * becomes dc=foobar,dc=server,dc=org
413
-	 *
414
-	 * @param string $dn
415
-	 * @return string
416
-	 */
417
-	public function getDomainDNFromDN($dn) {
418
-		$allParts = $this->ldap->explodeDN($dn, 0);
419
-		if ($allParts === false) {
420
-			//not a valid DN
421
-			return '';
422
-		}
423
-		$domainParts = [];
424
-		$dcFound = false;
425
-		foreach ($allParts as $part) {
426
-			if (!$dcFound && strpos($part, 'dc=') === 0) {
427
-				$dcFound = true;
428
-			}
429
-			if ($dcFound) {
430
-				$domainParts[] = $part;
431
-			}
432
-		}
433
-		return implode(',', $domainParts);
434
-	}
435
-
436
-	/**
437
-	 * returns the LDAP DN for the given internal Nextcloud name of the group
438
-	 *
439
-	 * @param string $name the Nextcloud name in question
440
-	 * @return string|false LDAP DN on success, otherwise false
441
-	 */
442
-	public function groupname2dn($name) {
443
-		return $this->groupMapper->getDNByName($name);
444
-	}
445
-
446
-	/**
447
-	 * returns the LDAP DN for the given internal Nextcloud name of the user
448
-	 *
449
-	 * @param string $name the Nextcloud name in question
450
-	 * @return string|false with the LDAP DN on success, otherwise false
451
-	 */
452
-	public function username2dn($name) {
453
-		$fdn = $this->userMapper->getDNByName($name);
454
-
455
-		//Check whether the DN belongs to the Base, to avoid issues on multi-
456
-		//server setups
457
-		if (is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
458
-			return $fdn;
459
-		}
460
-
461
-		return false;
462
-	}
463
-
464
-	/**
465
-	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
466
-	 *
467
-	 * @param string $fdn the dn of the group object
468
-	 * @param string $ldapName optional, the display name of the object
469
-	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
470
-	 * @throws \Exception
471
-	 */
472
-	public function dn2groupname($fdn, $ldapName = null) {
473
-		//To avoid bypassing the base DN settings under certain circumstances
474
-		//with the group support, check whether the provided DN matches one of
475
-		//the given Bases
476
-		if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
477
-			return false;
478
-		}
479
-
480
-		return $this->dn2ocname($fdn, $ldapName, false);
481
-	}
482
-
483
-	/**
484
-	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
485
-	 *
486
-	 * @param string $dn the dn of the user object
487
-	 * @param string $ldapName optional, the display name of the object
488
-	 * @return string|false with with the name to use in Nextcloud
489
-	 * @throws \Exception
490
-	 */
491
-	public function dn2username($fdn, $ldapName = null) {
492
-		//To avoid bypassing the base DN settings under certain circumstances
493
-		//with the group support, check whether the provided DN matches one of
494
-		//the given Bases
495
-		if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
496
-			return false;
497
-		}
498
-
499
-		return $this->dn2ocname($fdn, $ldapName, true);
500
-	}
501
-
502
-	/**
503
-	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
504
-	 *
505
-	 * @param string $fdn the dn of the user object
506
-	 * @param string|null $ldapName optional, the display name of the object
507
-	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
508
-	 * @param bool|null $newlyMapped
509
-	 * @param array|null $record
510
-	 * @return false|string with with the name to use in Nextcloud
511
-	 * @throws \Exception
512
-	 */
513
-	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
514
-		$newlyMapped = false;
515
-		if ($isUser) {
516
-			$mapper = $this->getUserMapper();
517
-			$nameAttribute = $this->connection->ldapUserDisplayName;
518
-			$filter = $this->connection->ldapUserFilter;
519
-		} else {
520
-			$mapper = $this->getGroupMapper();
521
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
522
-			$filter = $this->connection->ldapGroupFilter;
523
-		}
524
-
525
-		//let's try to retrieve the Nextcloud name from the mappings table
526
-		$ncName = $mapper->getNameByDN($fdn);
527
-		if (is_string($ncName)) {
528
-			return $ncName;
529
-		}
530
-
531
-		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
532
-		$uuid = $this->getUUID($fdn, $isUser, $record);
533
-		if (is_string($uuid)) {
534
-			$ncName = $mapper->getNameByUUID($uuid);
535
-			if (is_string($ncName)) {
536
-				$mapper->setDNbyUUID($fdn, $uuid);
537
-				return $ncName;
538
-			}
539
-		} else {
540
-			//If the UUID can't be detected something is foul.
541
-			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for ' . $fdn . '. Skipping.', ILogger::INFO);
542
-			return false;
543
-		}
544
-
545
-		if (is_null($ldapName)) {
546
-			$ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
547
-			if (!isset($ldapName[0]) && empty($ldapName[0])) {
548
-				\OCP\Util::writeLog('user_ldap', 'No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ILogger::INFO);
549
-				return false;
550
-			}
551
-			$ldapName = $ldapName[0];
552
-		}
553
-
554
-		if ($isUser) {
555
-			$usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
556
-			if ($usernameAttribute !== '') {
557
-				$username = $this->readAttribute($fdn, $usernameAttribute);
558
-				$username = $username[0];
559
-			} else {
560
-				$username = $uuid;
561
-			}
562
-			try {
563
-				$intName = $this->sanitizeUsername($username);
564
-			} catch (\InvalidArgumentException $e) {
565
-				\OC::$server->getLogger()->logException($e, [
566
-					'app' => 'user_ldap',
567
-					'level' => ILogger::WARN,
568
-				]);
569
-				// we don't attempt to set a username here. We can go for
570
-				// for an alternative 4 digit random number as we would append
571
-				// otherwise, however it's likely not enough space in bigger
572
-				// setups, and most importantly: this is not intended.
573
-				return false;
574
-			}
575
-		} else {
576
-			$intName = $ldapName;
577
-		}
578
-
579
-		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
580
-		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
581
-		//NOTE: mind, disabling cache affects only this instance! Using it
582
-		// outside of core user management will still cache the user as non-existing.
583
-		$originalTTL = $this->connection->ldapCacheTTL;
584
-		$this->connection->setConfiguration(['ldapCacheTTL' => 0]);
585
-		if ($intName !== ''
586
-			&& (($isUser && !$this->ncUserManager->userExists($intName))
587
-				|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
588
-			)
589
-		) {
590
-			$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
591
-			$newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
592
-			if ($newlyMapped) {
593
-				return $intName;
594
-			}
595
-		}
596
-
597
-		$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
598
-		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
599
-		if (is_string($altName)) {
600
-			if ($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
601
-				$newlyMapped = true;
602
-				return $altName;
603
-			}
604
-		}
605
-
606
-		//if everything else did not help..
607
-		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for ' . $fdn . '.', ILogger::INFO);
608
-		return false;
609
-	}
610
-
611
-	public function mapAndAnnounceIfApplicable(
612
-		AbstractMapping $mapper,
613
-		string $fdn,
614
-		string $name,
615
-		string $uuid,
616
-		bool $isUser
617
-	): bool {
618
-		if ($mapper->map($fdn, $name, $uuid)) {
619
-			if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
620
-				$this->cacheUserExists($name);
621
-				$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
622
-			} elseif (!$isUser) {
623
-				$this->cacheGroupExists($name);
624
-			}
625
-			return true;
626
-		}
627
-		return false;
628
-	}
629
-
630
-	/**
631
-	 * gives back the user names as they are used ownClod internally
632
-	 *
633
-	 * @param array $ldapUsers as returned by fetchList()
634
-	 * @return array an array with the user names to use in Nextcloud
635
-	 *
636
-	 * gives back the user names as they are used ownClod internally
637
-	 * @throws \Exception
638
-	 */
639
-	public function nextcloudUserNames($ldapUsers) {
640
-		return $this->ldap2NextcloudNames($ldapUsers, true);
641
-	}
642
-
643
-	/**
644
-	 * gives back the group names as they are used ownClod internally
645
-	 *
646
-	 * @param array $ldapGroups as returned by fetchList()
647
-	 * @return array an array with the group names to use in Nextcloud
648
-	 *
649
-	 * gives back the group names as they are used ownClod internally
650
-	 * @throws \Exception
651
-	 */
652
-	public function nextcloudGroupNames($ldapGroups) {
653
-		return $this->ldap2NextcloudNames($ldapGroups, false);
654
-	}
655
-
656
-	/**
657
-	 * @param array $ldapObjects as returned by fetchList()
658
-	 * @param bool $isUsers
659
-	 * @return array
660
-	 * @throws \Exception
661
-	 */
662
-	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
663
-		if ($isUsers) {
664
-			$nameAttribute = $this->connection->ldapUserDisplayName;
665
-			$sndAttribute = $this->connection->ldapUserDisplayName2;
666
-		} else {
667
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
668
-		}
669
-		$nextcloudNames = [];
670
-
671
-		foreach ($ldapObjects as $ldapObject) {
672
-			$nameByLDAP = null;
673
-			if (isset($ldapObject[$nameAttribute])
674
-				&& is_array($ldapObject[$nameAttribute])
675
-				&& isset($ldapObject[$nameAttribute][0])
676
-			) {
677
-				// might be set, but not necessarily. if so, we use it.
678
-				$nameByLDAP = $ldapObject[$nameAttribute][0];
679
-			}
680
-
681
-			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
682
-			if ($ncName) {
683
-				$nextcloudNames[] = $ncName;
684
-				if ($isUsers) {
685
-					$this->updateUserState($ncName);
686
-					//cache the user names so it does not need to be retrieved
687
-					//again later (e.g. sharing dialogue).
688
-					if (is_null($nameByLDAP)) {
689
-						continue;
690
-					}
691
-					$sndName = isset($ldapObject[$sndAttribute][0])
692
-						? $ldapObject[$sndAttribute][0] : '';
693
-					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
694
-				} elseif ($nameByLDAP !== null) {
695
-					$this->cacheGroupDisplayName($ncName, $nameByLDAP);
696
-				}
697
-			}
698
-		}
699
-		return $nextcloudNames;
700
-	}
701
-
702
-	/**
703
-	 * removes the deleted-flag of a user if it was set
704
-	 *
705
-	 * @param string $ncname
706
-	 * @throws \Exception
707
-	 */
708
-	public function updateUserState($ncname) {
709
-		$user = $this->userManager->get($ncname);
710
-		if ($user instanceof OfflineUser) {
711
-			$user->unmark();
712
-		}
713
-	}
714
-
715
-	/**
716
-	 * caches the user display name
717
-	 *
718
-	 * @param string $ocName the internal Nextcloud username
719
-	 * @param string|false $home the home directory path
720
-	 */
721
-	public function cacheUserHome($ocName, $home) {
722
-		$cacheKey = 'getHome' . $ocName;
723
-		$this->connection->writeToCache($cacheKey, $home);
724
-	}
725
-
726
-	/**
727
-	 * caches a user as existing
728
-	 *
729
-	 * @param string $ocName the internal Nextcloud username
730
-	 */
731
-	public function cacheUserExists($ocName) {
732
-		$this->connection->writeToCache('userExists' . $ocName, true);
733
-	}
734
-
735
-	/**
736
-	 * caches a group as existing
737
-	 */
738
-	public function cacheGroupExists(string $gid): void {
739
-		$this->connection->writeToCache('groupExists' . $gid, true);
740
-	}
741
-
742
-	/**
743
-	 * caches the user display name
744
-	 *
745
-	 * @param string $ocName the internal Nextcloud username
746
-	 * @param string $displayName the display name
747
-	 * @param string $displayName2 the second display name
748
-	 * @throws \Exception
749
-	 */
750
-	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
751
-		$user = $this->userManager->get($ocName);
752
-		if ($user === null) {
753
-			return;
754
-		}
755
-		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
756
-		$cacheKeyTrunk = 'getDisplayName';
757
-		$this->connection->writeToCache($cacheKeyTrunk . $ocName, $displayName);
758
-	}
759
-
760
-	public function cacheGroupDisplayName(string $ncName, string $displayName): void {
761
-		$cacheKey = 'group_getDisplayName' . $ncName;
762
-		$this->connection->writeToCache($cacheKey, $displayName);
763
-	}
764
-
765
-	/**
766
-	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
767
-	 *
768
-	 * @param string $name the display name of the object
769
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
770
-	 *
771
-	 * Instead of using this method directly, call
772
-	 * createAltInternalOwnCloudName($name, true)
773
-	 */
774
-	private function _createAltInternalOwnCloudNameForUsers($name) {
775
-		$attempts = 0;
776
-		//while loop is just a precaution. If a name is not generated within
777
-		//20 attempts, something else is very wrong. Avoids infinite loop.
778
-		while ($attempts < 20) {
779
-			$altName = $name . '_' . rand(1000, 9999);
780
-			if (!$this->ncUserManager->userExists($altName)) {
781
-				return $altName;
782
-			}
783
-			$attempts++;
784
-		}
785
-		return false;
786
-	}
787
-
788
-	/**
789
-	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
790
-	 *
791
-	 * @param string $name the display name of the object
792
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
793
-	 *
794
-	 * Instead of using this method directly, call
795
-	 * createAltInternalOwnCloudName($name, false)
796
-	 *
797
-	 * Group names are also used as display names, so we do a sequential
798
-	 * numbering, e.g. Developers_42 when there are 41 other groups called
799
-	 * "Developers"
800
-	 */
801
-	private function _createAltInternalOwnCloudNameForGroups($name) {
802
-		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
803
-		if (!$usedNames || count($usedNames) === 0) {
804
-			$lastNo = 1; //will become name_2
805
-		} else {
806
-			natsort($usedNames);
807
-			$lastName = array_pop($usedNames);
808
-			$lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
809
-		}
810
-		$altName = $name . '_' . (string)($lastNo + 1);
811
-		unset($usedNames);
812
-
813
-		$attempts = 1;
814
-		while ($attempts < 21) {
815
-			// Check to be really sure it is unique
816
-			// while loop is just a precaution. If a name is not generated within
817
-			// 20 attempts, something else is very wrong. Avoids infinite loop.
818
-			if (!\OC::$server->getGroupManager()->groupExists($altName)) {
819
-				return $altName;
820
-			}
821
-			$altName = $name . '_' . ($lastNo + $attempts);
822
-			$attempts++;
823
-		}
824
-		return false;
825
-	}
826
-
827
-	/**
828
-	 * creates a unique name for internal Nextcloud use.
829
-	 *
830
-	 * @param string $name the display name of the object
831
-	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
832
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
833
-	 */
834
-	private function createAltInternalOwnCloudName($name, $isUser) {
835
-		$originalTTL = $this->connection->ldapCacheTTL;
836
-		$this->connection->setConfiguration(['ldapCacheTTL' => 0]);
837
-		if ($isUser) {
838
-			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
839
-		} else {
840
-			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
841
-		}
842
-		$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
843
-
844
-		return $altName;
845
-	}
846
-
847
-	/**
848
-	 * fetches a list of users according to a provided loginName and utilizing
849
-	 * the login filter.
850
-	 */
851
-	public function fetchUsersByLoginName(string $loginName, array $attributes = ['dn']): array {
852
-		$loginName = $this->escapeFilterPart($loginName);
853
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
854
-		return $this->fetchListOfUsers($filter, $attributes);
855
-	}
856
-
857
-	/**
858
-	 * counts the number of users according to a provided loginName and
859
-	 * utilizing the login filter.
860
-	 *
861
-	 * @param string $loginName
862
-	 * @return int
863
-	 */
864
-	public function countUsersByLoginName($loginName) {
865
-		$loginName = $this->escapeFilterPart($loginName);
866
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
867
-		return $this->countUsers($filter);
868
-	}
869
-
870
-	/**
871
-	 * @throws \Exception
872
-	 */
873
-	public function fetchListOfUsers(string $filter, array $attr, int $limit = null, int $offset = null, bool $forceApplyAttributes = false): array {
874
-		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
875
-		$recordsToUpdate = $ldapRecords;
876
-		if (!$forceApplyAttributes) {
877
-			$isBackgroundJobModeAjax = $this->config
878
-					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
879
-			$listOfDNs = array_reduce($ldapRecords, function ($listOfDNs, $entry) {
880
-				$listOfDNs[] = $entry['dn'][0];
881
-				return $listOfDNs;
882
-			}, []);
883
-			$idsByDn = $this->userMapper->getListOfIdsByDn($listOfDNs);
884
-			$recordsToUpdate = array_filter($ldapRecords, function ($record) use ($isBackgroundJobModeAjax, $idsByDn) {
885
-				$newlyMapped = false;
886
-				$uid = $idsByDn[$record['dn'][0]] ?? null;
887
-				if ($uid === null) {
888
-					$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
889
-				}
890
-				if (is_string($uid)) {
891
-					$this->cacheUserExists($uid);
892
-				}
893
-				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
894
-			});
895
-		}
896
-		$this->batchApplyUserAttributes($recordsToUpdate);
897
-		return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
898
-	}
899
-
900
-	/**
901
-	 * provided with an array of LDAP user records the method will fetch the
902
-	 * user object and requests it to process the freshly fetched attributes and
903
-	 * and their values
904
-	 *
905
-	 * @param array $ldapRecords
906
-	 * @throws \Exception
907
-	 */
908
-	public function batchApplyUserAttributes(array $ldapRecords) {
909
-		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
910
-		foreach ($ldapRecords as $userRecord) {
911
-			if (!isset($userRecord[$displayNameAttribute])) {
912
-				// displayName is obligatory
913
-				continue;
914
-			}
915
-			$ocName = $this->dn2ocname($userRecord['dn'][0], null, true);
916
-			if ($ocName === false) {
917
-				continue;
918
-			}
919
-			$this->updateUserState($ocName);
920
-			$user = $this->userManager->get($ocName);
921
-			if ($user !== null) {
922
-				$user->processAttributes($userRecord);
923
-			} else {
924
-				\OC::$server->getLogger()->debug(
925
-					"The ldap user manager returned null for $ocName",
926
-					['app' => 'user_ldap']
927
-				);
928
-			}
929
-		}
930
-	}
931
-
932
-	/**
933
-	 * @param string $filter
934
-	 * @param string|string[] $attr
935
-	 * @param int $limit
936
-	 * @param int $offset
937
-	 * @return array
938
-	 */
939
-	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
940
-		$groupRecords = $this->searchGroups($filter, $attr, $limit, $offset);
941
-
942
-		$listOfDNs = array_reduce($groupRecords, function ($listOfDNs, $entry) {
943
-			$listOfDNs[] = $entry['dn'][0];
944
-			return $listOfDNs;
945
-		}, []);
946
-		$idsByDn = $this->groupMapper->getListOfIdsByDn($listOfDNs);
947
-
948
-		array_walk($groupRecords, function ($record) use ($idsByDn) {
949
-			$newlyMapped = false;
950
-			$gid = $uidsByDn[$record['dn'][0]] ?? null;
951
-			if ($gid === null) {
952
-				$gid = $this->dn2ocname($record['dn'][0], null, false, $newlyMapped, $record);
953
-			}
954
-			if (!$newlyMapped && is_string($gid)) {
955
-				$this->cacheGroupExists($gid);
956
-			}
957
-		});
958
-		return $this->fetchList($groupRecords, $this->manyAttributes($attr));
959
-	}
960
-
961
-	/**
962
-	 * @param array $list
963
-	 * @param bool $manyAttributes
964
-	 * @return array
965
-	 */
966
-	private function fetchList($list, $manyAttributes) {
967
-		if (is_array($list)) {
968
-			if ($manyAttributes) {
969
-				return $list;
970
-			} else {
971
-				$list = array_reduce($list, function ($carry, $item) {
972
-					$attribute = array_keys($item)[0];
973
-					$carry[] = $item[$attribute][0];
974
-					return $carry;
975
-				}, []);
976
-				return array_unique($list, SORT_LOCALE_STRING);
977
-			}
978
-		}
979
-
980
-		//error cause actually, maybe throw an exception in future.
981
-		return [];
982
-	}
983
-
984
-	/**
985
-	 * @throws ServerNotAvailableException
986
-	 */
987
-	public function searchUsers(string $filter, array $attr = null, int $limit = null, int $offset = null): array {
988
-		$result = [];
989
-		foreach ($this->connection->ldapBaseUsers as $base) {
990
-			$result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
991
-		}
992
-		return $result;
993
-	}
994
-
995
-	/**
996
-	 * @param string $filter
997
-	 * @param string|string[] $attr
998
-	 * @param int $limit
999
-	 * @param int $offset
1000
-	 * @return false|int
1001
-	 * @throws ServerNotAvailableException
1002
-	 */
1003
-	public function countUsers($filter, $attr = ['dn'], $limit = null, $offset = null) {
1004
-		$result = false;
1005
-		foreach ($this->connection->ldapBaseUsers as $base) {
1006
-			$count = $this->count($filter, [$base], $attr, $limit, $offset);
1007
-			$result = is_int($count) ? (int)$result + $count : $result;
1008
-		}
1009
-		return $result;
1010
-	}
1011
-
1012
-	/**
1013
-	 * executes an LDAP search, optimized for Groups
1014
-	 *
1015
-	 * @param string $filter the LDAP filter for the search
1016
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
1017
-	 * @param integer $limit
1018
-	 * @param integer $offset
1019
-	 * @return array with the search result
1020
-	 *
1021
-	 * Executes an LDAP search
1022
-	 * @throws ServerNotAvailableException
1023
-	 */
1024
-	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
1025
-		$result = [];
1026
-		foreach ($this->connection->ldapBaseGroups as $base) {
1027
-			$result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
1028
-		}
1029
-		return $result;
1030
-	}
1031
-
1032
-	/**
1033
-	 * returns the number of available groups
1034
-	 *
1035
-	 * @param string $filter the LDAP search filter
1036
-	 * @param string[] $attr optional
1037
-	 * @param int|null $limit
1038
-	 * @param int|null $offset
1039
-	 * @return int|bool
1040
-	 * @throws ServerNotAvailableException
1041
-	 */
1042
-	public function countGroups($filter, $attr = ['dn'], $limit = null, $offset = null) {
1043
-		$result = false;
1044
-		foreach ($this->connection->ldapBaseGroups as $base) {
1045
-			$count = $this->count($filter, [$base], $attr, $limit, $offset);
1046
-			$result = is_int($count) ? (int)$result + $count : $result;
1047
-		}
1048
-		return $result;
1049
-	}
1050
-
1051
-	/**
1052
-	 * returns the number of available objects on the base DN
1053
-	 *
1054
-	 * @param int|null $limit
1055
-	 * @param int|null $offset
1056
-	 * @return int|bool
1057
-	 * @throws ServerNotAvailableException
1058
-	 */
1059
-	public function countObjects($limit = null, $offset = null) {
1060
-		$result = false;
1061
-		foreach ($this->connection->ldapBase as $base) {
1062
-			$count = $this->count('objectclass=*', [$base], ['dn'], $limit, $offset);
1063
-			$result = is_int($count) ? (int)$result + $count : $result;
1064
-		}
1065
-		return $result;
1066
-	}
1067
-
1068
-	/**
1069
-	 * Returns the LDAP handler
1070
-	 *
1071
-	 * @throws \OC\ServerNotAvailableException
1072
-	 */
1073
-
1074
-	/**
1075
-	 * @return mixed
1076
-	 * @throws \OC\ServerNotAvailableException
1077
-	 */
1078
-	private function invokeLDAPMethod() {
1079
-		$arguments = func_get_args();
1080
-		$command = array_shift($arguments);
1081
-		$cr = array_shift($arguments);
1082
-		if (!method_exists($this->ldap, $command)) {
1083
-			return null;
1084
-		}
1085
-		array_unshift($arguments, $cr);
1086
-		// php no longer supports call-time pass-by-reference
1087
-		// thus cannot support controlPagedResultResponse as the third argument
1088
-		// is a reference
1089
-		$doMethod = function () use ($command, &$arguments) {
1090
-			if ($command == 'controlPagedResultResponse') {
1091
-				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1092
-			} else {
1093
-				return call_user_func_array([$this->ldap, $command], $arguments);
1094
-			}
1095
-		};
1096
-		try {
1097
-			$ret = $doMethod();
1098
-		} catch (ServerNotAvailableException $e) {
1099
-			/* Server connection lost, attempt to reestablish it
66
+    public const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
67
+
68
+    /** @var \OCA\User_LDAP\Connection */
69
+    public $connection;
70
+    /** @var Manager */
71
+    public $userManager;
72
+    //never ever check this var directly, always use getPagedSearchResultState
73
+    protected $pagedSearchedSuccessful;
74
+
75
+    /**
76
+     * protected $cookies = [];
77
+     *
78
+     * @var AbstractMapping $userMapper
79
+     */
80
+    protected $userMapper;
81
+
82
+    /**
83
+     * @var AbstractMapping $userMapper
84
+     */
85
+    protected $groupMapper;
86
+
87
+    /**
88
+     * @var \OCA\User_LDAP\Helper
89
+     */
90
+    private $helper;
91
+    /** @var IConfig */
92
+    private $config;
93
+    /** @var IUserManager */
94
+    private $ncUserManager;
95
+    /** @var string */
96
+    private $lastCookie = '';
97
+
98
+    public function __construct(
99
+        Connection $connection,
100
+        ILDAPWrapper $ldap,
101
+        Manager $userManager,
102
+        Helper $helper,
103
+        IConfig $config,
104
+        IUserManager $ncUserManager
105
+    ) {
106
+        parent::__construct($ldap);
107
+        $this->connection = $connection;
108
+        $this->userManager = $userManager;
109
+        $this->userManager->setLdapAccess($this);
110
+        $this->helper = $helper;
111
+        $this->config = $config;
112
+        $this->ncUserManager = $ncUserManager;
113
+    }
114
+
115
+    /**
116
+     * sets the User Mapper
117
+     *
118
+     * @param AbstractMapping $mapper
119
+     */
120
+    public function setUserMapper(AbstractMapping $mapper) {
121
+        $this->userMapper = $mapper;
122
+    }
123
+
124
+    /**
125
+     * returns the User Mapper
126
+     *
127
+     * @return AbstractMapping
128
+     * @throws \Exception
129
+     */
130
+    public function getUserMapper() {
131
+        if (is_null($this->userMapper)) {
132
+            throw new \Exception('UserMapper was not assigned to this Access instance.');
133
+        }
134
+        return $this->userMapper;
135
+    }
136
+
137
+    /**
138
+     * sets the Group Mapper
139
+     *
140
+     * @param AbstractMapping $mapper
141
+     */
142
+    public function setGroupMapper(AbstractMapping $mapper) {
143
+        $this->groupMapper = $mapper;
144
+    }
145
+
146
+    /**
147
+     * returns the Group Mapper
148
+     *
149
+     * @return AbstractMapping
150
+     * @throws \Exception
151
+     */
152
+    public function getGroupMapper() {
153
+        if (is_null($this->groupMapper)) {
154
+            throw new \Exception('GroupMapper was not assigned to this Access instance.');
155
+        }
156
+        return $this->groupMapper;
157
+    }
158
+
159
+    /**
160
+     * @return bool
161
+     */
162
+    private function checkConnection() {
163
+        return ($this->connection instanceof Connection);
164
+    }
165
+
166
+    /**
167
+     * returns the Connection instance
168
+     *
169
+     * @return \OCA\User_LDAP\Connection
170
+     */
171
+    public function getConnection() {
172
+        return $this->connection;
173
+    }
174
+
175
+    /**
176
+     * reads a given attribute for an LDAP record identified by a DN
177
+     *
178
+     * @param string $dn the record in question
179
+     * @param string $attr the attribute that shall be retrieved
180
+     *        if empty, just check the record's existence
181
+     * @param string $filter
182
+     * @return array|false an array of values on success or an empty
183
+     *          array if $attr is empty, false otherwise
184
+     * @throws ServerNotAvailableException
185
+     */
186
+    public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
187
+        if (!$this->checkConnection()) {
188
+            \OCP\Util::writeLog('user_ldap',
189
+                'No LDAP Connector assigned, access impossible for readAttribute.',
190
+                ILogger::WARN);
191
+            return false;
192
+        }
193
+        $cr = $this->connection->getConnectionResource();
194
+        if (!$this->ldap->isResource($cr)) {
195
+            //LDAP not available
196
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
197
+            return false;
198
+        }
199
+        //Cancel possibly running Paged Results operation, otherwise we run in
200
+        //LDAP protocol errors
201
+        $this->abandonPagedSearch();
202
+        // openLDAP requires that we init a new Paged Search. Not needed by AD,
203
+        // but does not hurt either.
204
+        $pagingSize = (int)$this->connection->ldapPagingSize;
205
+        // 0 won't result in replies, small numbers may leave out groups
206
+        // (cf. #12306), 500 is default for paging and should work everywhere.
207
+        $maxResults = $pagingSize > 20 ? $pagingSize : 500;
208
+        $attr = mb_strtolower($attr, 'UTF-8');
209
+        // the actual read attribute later may contain parameters on a ranged
210
+        // request, e.g. member;range=99-199. Depends on server reply.
211
+        $attrToRead = $attr;
212
+
213
+        $values = [];
214
+        $isRangeRequest = false;
215
+        do {
216
+            $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
217
+            if (is_bool($result)) {
218
+                // when an exists request was run and it was successful, an empty
219
+                // array must be returned
220
+                return $result ? [] : false;
221
+            }
222
+
223
+            if (!$isRangeRequest) {
224
+                $values = $this->extractAttributeValuesFromResult($result, $attr);
225
+                if (!empty($values)) {
226
+                    return $values;
227
+                }
228
+            }
229
+
230
+            $isRangeRequest = false;
231
+            $result = $this->extractRangeData($result, $attr);
232
+            if (!empty($result)) {
233
+                $normalizedResult = $this->extractAttributeValuesFromResult(
234
+                    [$attr => $result['values']],
235
+                    $attr
236
+                );
237
+                $values = array_merge($values, $normalizedResult);
238
+
239
+                if ($result['rangeHigh'] === '*') {
240
+                    // when server replies with * as high range value, there are
241
+                    // no more results left
242
+                    return $values;
243
+                } else {
244
+                    $low = $result['rangeHigh'] + 1;
245
+                    $attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
246
+                    $isRangeRequest = true;
247
+                }
248
+            }
249
+        } while ($isRangeRequest);
250
+
251
+        \OCP\Util::writeLog('user_ldap', 'Requested attribute ' . $attr . ' not found for ' . $dn, ILogger::DEBUG);
252
+        return false;
253
+    }
254
+
255
+    /**
256
+     * Runs an read operation against LDAP
257
+     *
258
+     * @param resource $cr the LDAP connection
259
+     * @param string $dn
260
+     * @param string $attribute
261
+     * @param string $filter
262
+     * @param int $maxResults
263
+     * @return array|bool false if there was any error, true if an exists check
264
+     *                    was performed and the requested DN found, array with the
265
+     *                    returned data on a successful usual operation
266
+     * @throws ServerNotAvailableException
267
+     */
268
+    public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
269
+        $this->initPagedSearch($filter, $dn, [$attribute], $maxResults, 0);
270
+        $dn = $this->helper->DNasBaseParameter($dn);
271
+        $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, [$attribute]);
272
+        if (!$this->ldap->isResource($rr)) {
273
+            if ($attribute !== '') {
274
+                //do not throw this message on userExists check, irritates
275
+                \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
276
+            }
277
+            //in case an error occurs , e.g. object does not exist
278
+            return false;
279
+        }
280
+        if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
281
+            \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
282
+            return true;
283
+        }
284
+        $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
285
+        if (!$this->ldap->isResource($er)) {
286
+            //did not match the filter, return false
287
+            return false;
288
+        }
289
+        //LDAP attributes are not case sensitive
290
+        $result = \OCP\Util::mb_array_change_key_case(
291
+            $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
292
+
293
+        return $result;
294
+    }
295
+
296
+    /**
297
+     * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
298
+     * data if present.
299
+     *
300
+     * @param array $result from ILDAPWrapper::getAttributes()
301
+     * @param string $attribute the attribute name that was read
302
+     * @return string[]
303
+     */
304
+    public function extractAttributeValuesFromResult($result, $attribute) {
305
+        $values = [];
306
+        if (isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
307
+            $lowercaseAttribute = strtolower($attribute);
308
+            for ($i = 0; $i < $result[$attribute]['count']; $i++) {
309
+                if ($this->resemblesDN($attribute)) {
310
+                    $values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
311
+                } elseif ($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
312
+                    $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
313
+                } else {
314
+                    $values[] = $result[$attribute][$i];
315
+                }
316
+            }
317
+        }
318
+        return $values;
319
+    }
320
+
321
+    /**
322
+     * Attempts to find ranged data in a getAttribute results and extracts the
323
+     * returned values as well as information on the range and full attribute
324
+     * name for further processing.
325
+     *
326
+     * @param array $result from ILDAPWrapper::getAttributes()
327
+     * @param string $attribute the attribute name that was read. Without ";range=…"
328
+     * @return array If a range was detected with keys 'values', 'attributeName',
329
+     *               'attributeFull' and 'rangeHigh', otherwise empty.
330
+     */
331
+    public function extractRangeData($result, $attribute) {
332
+        $keys = array_keys($result);
333
+        foreach ($keys as $key) {
334
+            if ($key !== $attribute && strpos($key, $attribute) === 0) {
335
+                $queryData = explode(';', $key);
336
+                if (strpos($queryData[1], 'range=') === 0) {
337
+                    $high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
338
+                    $data = [
339
+                        'values' => $result[$key],
340
+                        'attributeName' => $queryData[0],
341
+                        'attributeFull' => $key,
342
+                        'rangeHigh' => $high,
343
+                    ];
344
+                    return $data;
345
+                }
346
+            }
347
+        }
348
+        return [];
349
+    }
350
+
351
+    /**
352
+     * Set password for an LDAP user identified by a DN
353
+     *
354
+     * @param string $userDN the user in question
355
+     * @param string $password the new password
356
+     * @return bool
357
+     * @throws HintException
358
+     * @throws \Exception
359
+     */
360
+    public function setPassword($userDN, $password) {
361
+        if ((int)$this->connection->turnOnPasswordChange !== 1) {
362
+            throw new \Exception('LDAP password changes are disabled.');
363
+        }
364
+        $cr = $this->connection->getConnectionResource();
365
+        if (!$this->ldap->isResource($cr)) {
366
+            //LDAP not available
367
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
368
+            return false;
369
+        }
370
+        try {
371
+            // try PASSWD extended operation first
372
+            return @$this->invokeLDAPMethod('exopPasswd', $cr, $userDN, '', $password) ||
373
+                @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
374
+        } catch (ConstraintViolationException $e) {
375
+            throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ') . $e->getMessage(), $e->getCode());
376
+        }
377
+    }
378
+
379
+    /**
380
+     * checks whether the given attributes value is probably a DN
381
+     *
382
+     * @param string $attr the attribute in question
383
+     * @return boolean if so true, otherwise false
384
+     */
385
+    private function resemblesDN($attr) {
386
+        $resemblingAttributes = [
387
+            'dn',
388
+            'uniquemember',
389
+            'member',
390
+            // memberOf is an "operational" attribute, without a definition in any RFC
391
+            'memberof'
392
+        ];
393
+        return in_array($attr, $resemblingAttributes);
394
+    }
395
+
396
+    /**
397
+     * checks whether the given string is probably a DN
398
+     *
399
+     * @param string $string
400
+     * @return boolean
401
+     */
402
+    public function stringResemblesDN($string) {
403
+        $r = $this->ldap->explodeDN($string, 0);
404
+        // if exploding a DN succeeds and does not end up in
405
+        // an empty array except for $r[count] being 0.
406
+        return (is_array($r) && count($r) > 1);
407
+    }
408
+
409
+    /**
410
+     * returns a DN-string that is cleaned from not domain parts, e.g.
411
+     * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
412
+     * becomes dc=foobar,dc=server,dc=org
413
+     *
414
+     * @param string $dn
415
+     * @return string
416
+     */
417
+    public function getDomainDNFromDN($dn) {
418
+        $allParts = $this->ldap->explodeDN($dn, 0);
419
+        if ($allParts === false) {
420
+            //not a valid DN
421
+            return '';
422
+        }
423
+        $domainParts = [];
424
+        $dcFound = false;
425
+        foreach ($allParts as $part) {
426
+            if (!$dcFound && strpos($part, 'dc=') === 0) {
427
+                $dcFound = true;
428
+            }
429
+            if ($dcFound) {
430
+                $domainParts[] = $part;
431
+            }
432
+        }
433
+        return implode(',', $domainParts);
434
+    }
435
+
436
+    /**
437
+     * returns the LDAP DN for the given internal Nextcloud name of the group
438
+     *
439
+     * @param string $name the Nextcloud name in question
440
+     * @return string|false LDAP DN on success, otherwise false
441
+     */
442
+    public function groupname2dn($name) {
443
+        return $this->groupMapper->getDNByName($name);
444
+    }
445
+
446
+    /**
447
+     * returns the LDAP DN for the given internal Nextcloud name of the user
448
+     *
449
+     * @param string $name the Nextcloud name in question
450
+     * @return string|false with the LDAP DN on success, otherwise false
451
+     */
452
+    public function username2dn($name) {
453
+        $fdn = $this->userMapper->getDNByName($name);
454
+
455
+        //Check whether the DN belongs to the Base, to avoid issues on multi-
456
+        //server setups
457
+        if (is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
458
+            return $fdn;
459
+        }
460
+
461
+        return false;
462
+    }
463
+
464
+    /**
465
+     * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
466
+     *
467
+     * @param string $fdn the dn of the group object
468
+     * @param string $ldapName optional, the display name of the object
469
+     * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
470
+     * @throws \Exception
471
+     */
472
+    public function dn2groupname($fdn, $ldapName = null) {
473
+        //To avoid bypassing the base DN settings under certain circumstances
474
+        //with the group support, check whether the provided DN matches one of
475
+        //the given Bases
476
+        if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
477
+            return false;
478
+        }
479
+
480
+        return $this->dn2ocname($fdn, $ldapName, false);
481
+    }
482
+
483
+    /**
484
+     * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
485
+     *
486
+     * @param string $dn the dn of the user object
487
+     * @param string $ldapName optional, the display name of the object
488
+     * @return string|false with with the name to use in Nextcloud
489
+     * @throws \Exception
490
+     */
491
+    public function dn2username($fdn, $ldapName = null) {
492
+        //To avoid bypassing the base DN settings under certain circumstances
493
+        //with the group support, check whether the provided DN matches one of
494
+        //the given Bases
495
+        if (!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
496
+            return false;
497
+        }
498
+
499
+        return $this->dn2ocname($fdn, $ldapName, true);
500
+    }
501
+
502
+    /**
503
+     * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
504
+     *
505
+     * @param string $fdn the dn of the user object
506
+     * @param string|null $ldapName optional, the display name of the object
507
+     * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
508
+     * @param bool|null $newlyMapped
509
+     * @param array|null $record
510
+     * @return false|string with with the name to use in Nextcloud
511
+     * @throws \Exception
512
+     */
513
+    public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
514
+        $newlyMapped = false;
515
+        if ($isUser) {
516
+            $mapper = $this->getUserMapper();
517
+            $nameAttribute = $this->connection->ldapUserDisplayName;
518
+            $filter = $this->connection->ldapUserFilter;
519
+        } else {
520
+            $mapper = $this->getGroupMapper();
521
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
522
+            $filter = $this->connection->ldapGroupFilter;
523
+        }
524
+
525
+        //let's try to retrieve the Nextcloud name from the mappings table
526
+        $ncName = $mapper->getNameByDN($fdn);
527
+        if (is_string($ncName)) {
528
+            return $ncName;
529
+        }
530
+
531
+        //second try: get the UUID and check if it is known. Then, update the DN and return the name.
532
+        $uuid = $this->getUUID($fdn, $isUser, $record);
533
+        if (is_string($uuid)) {
534
+            $ncName = $mapper->getNameByUUID($uuid);
535
+            if (is_string($ncName)) {
536
+                $mapper->setDNbyUUID($fdn, $uuid);
537
+                return $ncName;
538
+            }
539
+        } else {
540
+            //If the UUID can't be detected something is foul.
541
+            \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for ' . $fdn . '. Skipping.', ILogger::INFO);
542
+            return false;
543
+        }
544
+
545
+        if (is_null($ldapName)) {
546
+            $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
547
+            if (!isset($ldapName[0]) && empty($ldapName[0])) {
548
+                \OCP\Util::writeLog('user_ldap', 'No or empty name for ' . $fdn . ' with filter ' . $filter . '.', ILogger::INFO);
549
+                return false;
550
+            }
551
+            $ldapName = $ldapName[0];
552
+        }
553
+
554
+        if ($isUser) {
555
+            $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
556
+            if ($usernameAttribute !== '') {
557
+                $username = $this->readAttribute($fdn, $usernameAttribute);
558
+                $username = $username[0];
559
+            } else {
560
+                $username = $uuid;
561
+            }
562
+            try {
563
+                $intName = $this->sanitizeUsername($username);
564
+            } catch (\InvalidArgumentException $e) {
565
+                \OC::$server->getLogger()->logException($e, [
566
+                    'app' => 'user_ldap',
567
+                    'level' => ILogger::WARN,
568
+                ]);
569
+                // we don't attempt to set a username here. We can go for
570
+                // for an alternative 4 digit random number as we would append
571
+                // otherwise, however it's likely not enough space in bigger
572
+                // setups, and most importantly: this is not intended.
573
+                return false;
574
+            }
575
+        } else {
576
+            $intName = $ldapName;
577
+        }
578
+
579
+        //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
580
+        //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
581
+        //NOTE: mind, disabling cache affects only this instance! Using it
582
+        // outside of core user management will still cache the user as non-existing.
583
+        $originalTTL = $this->connection->ldapCacheTTL;
584
+        $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
585
+        if ($intName !== ''
586
+            && (($isUser && !$this->ncUserManager->userExists($intName))
587
+                || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
588
+            )
589
+        ) {
590
+            $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
591
+            $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
592
+            if ($newlyMapped) {
593
+                return $intName;
594
+            }
595
+        }
596
+
597
+        $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
598
+        $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
599
+        if (is_string($altName)) {
600
+            if ($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
601
+                $newlyMapped = true;
602
+                return $altName;
603
+            }
604
+        }
605
+
606
+        //if everything else did not help..
607
+        \OCP\Util::writeLog('user_ldap', 'Could not create unique name for ' . $fdn . '.', ILogger::INFO);
608
+        return false;
609
+    }
610
+
611
+    public function mapAndAnnounceIfApplicable(
612
+        AbstractMapping $mapper,
613
+        string $fdn,
614
+        string $name,
615
+        string $uuid,
616
+        bool $isUser
617
+    ): bool {
618
+        if ($mapper->map($fdn, $name, $uuid)) {
619
+            if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
620
+                $this->cacheUserExists($name);
621
+                $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
622
+            } elseif (!$isUser) {
623
+                $this->cacheGroupExists($name);
624
+            }
625
+            return true;
626
+        }
627
+        return false;
628
+    }
629
+
630
+    /**
631
+     * gives back the user names as they are used ownClod internally
632
+     *
633
+     * @param array $ldapUsers as returned by fetchList()
634
+     * @return array an array with the user names to use in Nextcloud
635
+     *
636
+     * gives back the user names as they are used ownClod internally
637
+     * @throws \Exception
638
+     */
639
+    public function nextcloudUserNames($ldapUsers) {
640
+        return $this->ldap2NextcloudNames($ldapUsers, true);
641
+    }
642
+
643
+    /**
644
+     * gives back the group names as they are used ownClod internally
645
+     *
646
+     * @param array $ldapGroups as returned by fetchList()
647
+     * @return array an array with the group names to use in Nextcloud
648
+     *
649
+     * gives back the group names as they are used ownClod internally
650
+     * @throws \Exception
651
+     */
652
+    public function nextcloudGroupNames($ldapGroups) {
653
+        return $this->ldap2NextcloudNames($ldapGroups, false);
654
+    }
655
+
656
+    /**
657
+     * @param array $ldapObjects as returned by fetchList()
658
+     * @param bool $isUsers
659
+     * @return array
660
+     * @throws \Exception
661
+     */
662
+    private function ldap2NextcloudNames($ldapObjects, $isUsers) {
663
+        if ($isUsers) {
664
+            $nameAttribute = $this->connection->ldapUserDisplayName;
665
+            $sndAttribute = $this->connection->ldapUserDisplayName2;
666
+        } else {
667
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
668
+        }
669
+        $nextcloudNames = [];
670
+
671
+        foreach ($ldapObjects as $ldapObject) {
672
+            $nameByLDAP = null;
673
+            if (isset($ldapObject[$nameAttribute])
674
+                && is_array($ldapObject[$nameAttribute])
675
+                && isset($ldapObject[$nameAttribute][0])
676
+            ) {
677
+                // might be set, but not necessarily. if so, we use it.
678
+                $nameByLDAP = $ldapObject[$nameAttribute][0];
679
+            }
680
+
681
+            $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
682
+            if ($ncName) {
683
+                $nextcloudNames[] = $ncName;
684
+                if ($isUsers) {
685
+                    $this->updateUserState($ncName);
686
+                    //cache the user names so it does not need to be retrieved
687
+                    //again later (e.g. sharing dialogue).
688
+                    if (is_null($nameByLDAP)) {
689
+                        continue;
690
+                    }
691
+                    $sndName = isset($ldapObject[$sndAttribute][0])
692
+                        ? $ldapObject[$sndAttribute][0] : '';
693
+                    $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
694
+                } elseif ($nameByLDAP !== null) {
695
+                    $this->cacheGroupDisplayName($ncName, $nameByLDAP);
696
+                }
697
+            }
698
+        }
699
+        return $nextcloudNames;
700
+    }
701
+
702
+    /**
703
+     * removes the deleted-flag of a user if it was set
704
+     *
705
+     * @param string $ncname
706
+     * @throws \Exception
707
+     */
708
+    public function updateUserState($ncname) {
709
+        $user = $this->userManager->get($ncname);
710
+        if ($user instanceof OfflineUser) {
711
+            $user->unmark();
712
+        }
713
+    }
714
+
715
+    /**
716
+     * caches the user display name
717
+     *
718
+     * @param string $ocName the internal Nextcloud username
719
+     * @param string|false $home the home directory path
720
+     */
721
+    public function cacheUserHome($ocName, $home) {
722
+        $cacheKey = 'getHome' . $ocName;
723
+        $this->connection->writeToCache($cacheKey, $home);
724
+    }
725
+
726
+    /**
727
+     * caches a user as existing
728
+     *
729
+     * @param string $ocName the internal Nextcloud username
730
+     */
731
+    public function cacheUserExists($ocName) {
732
+        $this->connection->writeToCache('userExists' . $ocName, true);
733
+    }
734
+
735
+    /**
736
+     * caches a group as existing
737
+     */
738
+    public function cacheGroupExists(string $gid): void {
739
+        $this->connection->writeToCache('groupExists' . $gid, true);
740
+    }
741
+
742
+    /**
743
+     * caches the user display name
744
+     *
745
+     * @param string $ocName the internal Nextcloud username
746
+     * @param string $displayName the display name
747
+     * @param string $displayName2 the second display name
748
+     * @throws \Exception
749
+     */
750
+    public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
751
+        $user = $this->userManager->get($ocName);
752
+        if ($user === null) {
753
+            return;
754
+        }
755
+        $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
756
+        $cacheKeyTrunk = 'getDisplayName';
757
+        $this->connection->writeToCache($cacheKeyTrunk . $ocName, $displayName);
758
+    }
759
+
760
+    public function cacheGroupDisplayName(string $ncName, string $displayName): void {
761
+        $cacheKey = 'group_getDisplayName' . $ncName;
762
+        $this->connection->writeToCache($cacheKey, $displayName);
763
+    }
764
+
765
+    /**
766
+     * creates a unique name for internal Nextcloud use for users. Don't call it directly.
767
+     *
768
+     * @param string $name the display name of the object
769
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
770
+     *
771
+     * Instead of using this method directly, call
772
+     * createAltInternalOwnCloudName($name, true)
773
+     */
774
+    private function _createAltInternalOwnCloudNameForUsers($name) {
775
+        $attempts = 0;
776
+        //while loop is just a precaution. If a name is not generated within
777
+        //20 attempts, something else is very wrong. Avoids infinite loop.
778
+        while ($attempts < 20) {
779
+            $altName = $name . '_' . rand(1000, 9999);
780
+            if (!$this->ncUserManager->userExists($altName)) {
781
+                return $altName;
782
+            }
783
+            $attempts++;
784
+        }
785
+        return false;
786
+    }
787
+
788
+    /**
789
+     * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
790
+     *
791
+     * @param string $name the display name of the object
792
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
793
+     *
794
+     * Instead of using this method directly, call
795
+     * createAltInternalOwnCloudName($name, false)
796
+     *
797
+     * Group names are also used as display names, so we do a sequential
798
+     * numbering, e.g. Developers_42 when there are 41 other groups called
799
+     * "Developers"
800
+     */
801
+    private function _createAltInternalOwnCloudNameForGroups($name) {
802
+        $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
803
+        if (!$usedNames || count($usedNames) === 0) {
804
+            $lastNo = 1; //will become name_2
805
+        } else {
806
+            natsort($usedNames);
807
+            $lastName = array_pop($usedNames);
808
+            $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
809
+        }
810
+        $altName = $name . '_' . (string)($lastNo + 1);
811
+        unset($usedNames);
812
+
813
+        $attempts = 1;
814
+        while ($attempts < 21) {
815
+            // Check to be really sure it is unique
816
+            // while loop is just a precaution. If a name is not generated within
817
+            // 20 attempts, something else is very wrong. Avoids infinite loop.
818
+            if (!\OC::$server->getGroupManager()->groupExists($altName)) {
819
+                return $altName;
820
+            }
821
+            $altName = $name . '_' . ($lastNo + $attempts);
822
+            $attempts++;
823
+        }
824
+        return false;
825
+    }
826
+
827
+    /**
828
+     * creates a unique name for internal Nextcloud use.
829
+     *
830
+     * @param string $name the display name of the object
831
+     * @param boolean $isUser whether name should be created for a user (true) or a group (false)
832
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
833
+     */
834
+    private function createAltInternalOwnCloudName($name, $isUser) {
835
+        $originalTTL = $this->connection->ldapCacheTTL;
836
+        $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
837
+        if ($isUser) {
838
+            $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
839
+        } else {
840
+            $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
841
+        }
842
+        $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
843
+
844
+        return $altName;
845
+    }
846
+
847
+    /**
848
+     * fetches a list of users according to a provided loginName and utilizing
849
+     * the login filter.
850
+     */
851
+    public function fetchUsersByLoginName(string $loginName, array $attributes = ['dn']): array {
852
+        $loginName = $this->escapeFilterPart($loginName);
853
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
854
+        return $this->fetchListOfUsers($filter, $attributes);
855
+    }
856
+
857
+    /**
858
+     * counts the number of users according to a provided loginName and
859
+     * utilizing the login filter.
860
+     *
861
+     * @param string $loginName
862
+     * @return int
863
+     */
864
+    public function countUsersByLoginName($loginName) {
865
+        $loginName = $this->escapeFilterPart($loginName);
866
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
867
+        return $this->countUsers($filter);
868
+    }
869
+
870
+    /**
871
+     * @throws \Exception
872
+     */
873
+    public function fetchListOfUsers(string $filter, array $attr, int $limit = null, int $offset = null, bool $forceApplyAttributes = false): array {
874
+        $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
875
+        $recordsToUpdate = $ldapRecords;
876
+        if (!$forceApplyAttributes) {
877
+            $isBackgroundJobModeAjax = $this->config
878
+                    ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
879
+            $listOfDNs = array_reduce($ldapRecords, function ($listOfDNs, $entry) {
880
+                $listOfDNs[] = $entry['dn'][0];
881
+                return $listOfDNs;
882
+            }, []);
883
+            $idsByDn = $this->userMapper->getListOfIdsByDn($listOfDNs);
884
+            $recordsToUpdate = array_filter($ldapRecords, function ($record) use ($isBackgroundJobModeAjax, $idsByDn) {
885
+                $newlyMapped = false;
886
+                $uid = $idsByDn[$record['dn'][0]] ?? null;
887
+                if ($uid === null) {
888
+                    $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
889
+                }
890
+                if (is_string($uid)) {
891
+                    $this->cacheUserExists($uid);
892
+                }
893
+                return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
894
+            });
895
+        }
896
+        $this->batchApplyUserAttributes($recordsToUpdate);
897
+        return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
898
+    }
899
+
900
+    /**
901
+     * provided with an array of LDAP user records the method will fetch the
902
+     * user object and requests it to process the freshly fetched attributes and
903
+     * and their values
904
+     *
905
+     * @param array $ldapRecords
906
+     * @throws \Exception
907
+     */
908
+    public function batchApplyUserAttributes(array $ldapRecords) {
909
+        $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
910
+        foreach ($ldapRecords as $userRecord) {
911
+            if (!isset($userRecord[$displayNameAttribute])) {
912
+                // displayName is obligatory
913
+                continue;
914
+            }
915
+            $ocName = $this->dn2ocname($userRecord['dn'][0], null, true);
916
+            if ($ocName === false) {
917
+                continue;
918
+            }
919
+            $this->updateUserState($ocName);
920
+            $user = $this->userManager->get($ocName);
921
+            if ($user !== null) {
922
+                $user->processAttributes($userRecord);
923
+            } else {
924
+                \OC::$server->getLogger()->debug(
925
+                    "The ldap user manager returned null for $ocName",
926
+                    ['app' => 'user_ldap']
927
+                );
928
+            }
929
+        }
930
+    }
931
+
932
+    /**
933
+     * @param string $filter
934
+     * @param string|string[] $attr
935
+     * @param int $limit
936
+     * @param int $offset
937
+     * @return array
938
+     */
939
+    public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
940
+        $groupRecords = $this->searchGroups($filter, $attr, $limit, $offset);
941
+
942
+        $listOfDNs = array_reduce($groupRecords, function ($listOfDNs, $entry) {
943
+            $listOfDNs[] = $entry['dn'][0];
944
+            return $listOfDNs;
945
+        }, []);
946
+        $idsByDn = $this->groupMapper->getListOfIdsByDn($listOfDNs);
947
+
948
+        array_walk($groupRecords, function ($record) use ($idsByDn) {
949
+            $newlyMapped = false;
950
+            $gid = $uidsByDn[$record['dn'][0]] ?? null;
951
+            if ($gid === null) {
952
+                $gid = $this->dn2ocname($record['dn'][0], null, false, $newlyMapped, $record);
953
+            }
954
+            if (!$newlyMapped && is_string($gid)) {
955
+                $this->cacheGroupExists($gid);
956
+            }
957
+        });
958
+        return $this->fetchList($groupRecords, $this->manyAttributes($attr));
959
+    }
960
+
961
+    /**
962
+     * @param array $list
963
+     * @param bool $manyAttributes
964
+     * @return array
965
+     */
966
+    private function fetchList($list, $manyAttributes) {
967
+        if (is_array($list)) {
968
+            if ($manyAttributes) {
969
+                return $list;
970
+            } else {
971
+                $list = array_reduce($list, function ($carry, $item) {
972
+                    $attribute = array_keys($item)[0];
973
+                    $carry[] = $item[$attribute][0];
974
+                    return $carry;
975
+                }, []);
976
+                return array_unique($list, SORT_LOCALE_STRING);
977
+            }
978
+        }
979
+
980
+        //error cause actually, maybe throw an exception in future.
981
+        return [];
982
+    }
983
+
984
+    /**
985
+     * @throws ServerNotAvailableException
986
+     */
987
+    public function searchUsers(string $filter, array $attr = null, int $limit = null, int $offset = null): array {
988
+        $result = [];
989
+        foreach ($this->connection->ldapBaseUsers as $base) {
990
+            $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
991
+        }
992
+        return $result;
993
+    }
994
+
995
+    /**
996
+     * @param string $filter
997
+     * @param string|string[] $attr
998
+     * @param int $limit
999
+     * @param int $offset
1000
+     * @return false|int
1001
+     * @throws ServerNotAvailableException
1002
+     */
1003
+    public function countUsers($filter, $attr = ['dn'], $limit = null, $offset = null) {
1004
+        $result = false;
1005
+        foreach ($this->connection->ldapBaseUsers as $base) {
1006
+            $count = $this->count($filter, [$base], $attr, $limit, $offset);
1007
+            $result = is_int($count) ? (int)$result + $count : $result;
1008
+        }
1009
+        return $result;
1010
+    }
1011
+
1012
+    /**
1013
+     * executes an LDAP search, optimized for Groups
1014
+     *
1015
+     * @param string $filter the LDAP filter for the search
1016
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
1017
+     * @param integer $limit
1018
+     * @param integer $offset
1019
+     * @return array with the search result
1020
+     *
1021
+     * Executes an LDAP search
1022
+     * @throws ServerNotAvailableException
1023
+     */
1024
+    public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
1025
+        $result = [];
1026
+        foreach ($this->connection->ldapBaseGroups as $base) {
1027
+            $result = array_merge($result, $this->search($filter, $base, $attr, $limit, $offset));
1028
+        }
1029
+        return $result;
1030
+    }
1031
+
1032
+    /**
1033
+     * returns the number of available groups
1034
+     *
1035
+     * @param string $filter the LDAP search filter
1036
+     * @param string[] $attr optional
1037
+     * @param int|null $limit
1038
+     * @param int|null $offset
1039
+     * @return int|bool
1040
+     * @throws ServerNotAvailableException
1041
+     */
1042
+    public function countGroups($filter, $attr = ['dn'], $limit = null, $offset = null) {
1043
+        $result = false;
1044
+        foreach ($this->connection->ldapBaseGroups as $base) {
1045
+            $count = $this->count($filter, [$base], $attr, $limit, $offset);
1046
+            $result = is_int($count) ? (int)$result + $count : $result;
1047
+        }
1048
+        return $result;
1049
+    }
1050
+
1051
+    /**
1052
+     * returns the number of available objects on the base DN
1053
+     *
1054
+     * @param int|null $limit
1055
+     * @param int|null $offset
1056
+     * @return int|bool
1057
+     * @throws ServerNotAvailableException
1058
+     */
1059
+    public function countObjects($limit = null, $offset = null) {
1060
+        $result = false;
1061
+        foreach ($this->connection->ldapBase as $base) {
1062
+            $count = $this->count('objectclass=*', [$base], ['dn'], $limit, $offset);
1063
+            $result = is_int($count) ? (int)$result + $count : $result;
1064
+        }
1065
+        return $result;
1066
+    }
1067
+
1068
+    /**
1069
+     * Returns the LDAP handler
1070
+     *
1071
+     * @throws \OC\ServerNotAvailableException
1072
+     */
1073
+
1074
+    /**
1075
+     * @return mixed
1076
+     * @throws \OC\ServerNotAvailableException
1077
+     */
1078
+    private function invokeLDAPMethod() {
1079
+        $arguments = func_get_args();
1080
+        $command = array_shift($arguments);
1081
+        $cr = array_shift($arguments);
1082
+        if (!method_exists($this->ldap, $command)) {
1083
+            return null;
1084
+        }
1085
+        array_unshift($arguments, $cr);
1086
+        // php no longer supports call-time pass-by-reference
1087
+        // thus cannot support controlPagedResultResponse as the third argument
1088
+        // is a reference
1089
+        $doMethod = function () use ($command, &$arguments) {
1090
+            if ($command == 'controlPagedResultResponse') {
1091
+                throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1092
+            } else {
1093
+                return call_user_func_array([$this->ldap, $command], $arguments);
1094
+            }
1095
+        };
1096
+        try {
1097
+            $ret = $doMethod();
1098
+        } catch (ServerNotAvailableException $e) {
1099
+            /* Server connection lost, attempt to reestablish it
1100 1100
 			 * Maybe implement exponential backoff?
1101 1101
 			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1102 1102
 			 */
1103
-			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
1104
-			$this->connection->resetConnectionResource();
1105
-			$cr = $this->connection->getConnectionResource();
1106
-
1107
-			if (!$this->ldap->isResource($cr)) {
1108
-				// Seems like we didn't find any resource.
1109
-				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
1110
-				throw $e;
1111
-			}
1112
-
1113
-			$arguments[0] = $cr;
1114
-			$ret = $doMethod();
1115
-		}
1116
-		return $ret;
1117
-	}
1118
-
1119
-	/**
1120
-	 * retrieved. Results will according to the order in the array.
1121
-	 *
1122
-	 * @param string $filter
1123
-	 * @param string $base
1124
-	 * @param string[] $attr
1125
-	 * @param int|null $limit optional, maximum results to be counted
1126
-	 * @param int|null $offset optional, a starting point
1127
-	 * @return array|false array with the search result as first value and pagedSearchOK as
1128
-	 * second | false if not successful
1129
-	 * @throws ServerNotAvailableException
1130
-	 */
1131
-	private function executeSearch(
1132
-		string $filter,
1133
-		string $base,
1134
-		?array &$attr,
1135
-		?int $limit,
1136
-		?int $offset
1137
-	) {
1138
-		// See if we have a resource, in case not cancel with message
1139
-		$cr = $this->connection->getConnectionResource();
1140
-		if (!$this->ldap->isResource($cr)) {
1141
-			// Seems like we didn't find any resource.
1142
-			// Return an empty array just like before.
1143
-			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
1144
-			return false;
1145
-		}
1146
-
1147
-		//check whether paged search should be attempted
1148
-		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, (int)$offset);
1149
-
1150
-		$sr = $this->invokeLDAPMethod('search', $cr, $base, $filter, $attr);
1151
-		// cannot use $cr anymore, might have changed in the previous call!
1152
-		$error = $this->ldap->errno($this->connection->getConnectionResource());
1153
-		if (!$this->ldap->isResource($sr) || $error !== 0) {
1154
-			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  ' . print_r($pagedSearchOK, true), ILogger::ERROR);
1155
-			return false;
1156
-		}
1157
-
1158
-		return [$sr, $pagedSearchOK];
1159
-	}
1160
-
1161
-	/**
1162
-	 * processes an LDAP paged search operation
1163
-	 *
1164
-	 * @param resource $sr the array containing the LDAP search resources
1165
-	 * @param int $foundItems number of results in the single search operation
1166
-	 * @param int $limit maximum results to be counted
1167
-	 * @param bool $pagedSearchOK whether a paged search has been executed
1168
-	 * @param bool $skipHandling required for paged search when cookies to
1169
-	 * prior results need to be gained
1170
-	 * @return bool cookie validity, true if we have more pages, false otherwise.
1171
-	 * @throws ServerNotAvailableException
1172
-	 */
1173
-	private function processPagedSearchStatus(
1174
-		$sr,
1175
-		int $foundItems,
1176
-		int $limit,
1177
-		bool $pagedSearchOK,
1178
-		bool $skipHandling
1179
-	): bool {
1180
-		$cookie = null;
1181
-		if ($pagedSearchOK) {
1182
-			$cr = $this->connection->getConnectionResource();
1183
-			if ($this->ldap->controlPagedResultResponse($cr, $sr, $cookie)) {
1184
-				$this->lastCookie = $cookie;
1185
-			}
1186
-
1187
-			//browsing through prior pages to get the cookie for the new one
1188
-			if ($skipHandling) {
1189
-				return false;
1190
-			}
1191
-			// if count is bigger, then the server does not support
1192
-			// paged search. Instead, he did a normal search. We set a
1193
-			// flag here, so the callee knows how to deal with it.
1194
-			if ($foundItems <= $limit) {
1195
-				$this->pagedSearchedSuccessful = true;
1196
-			}
1197
-		} else {
1198
-			if (!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1199
-				\OC::$server->getLogger()->debug(
1200
-					'Paged search was not available',
1201
-					['app' => 'user_ldap']
1202
-				);
1203
-			}
1204
-		}
1205
-		/* ++ Fixing RHDS searches with pages with zero results ++
1103
+            \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
1104
+            $this->connection->resetConnectionResource();
1105
+            $cr = $this->connection->getConnectionResource();
1106
+
1107
+            if (!$this->ldap->isResource($cr)) {
1108
+                // Seems like we didn't find any resource.
1109
+                \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
1110
+                throw $e;
1111
+            }
1112
+
1113
+            $arguments[0] = $cr;
1114
+            $ret = $doMethod();
1115
+        }
1116
+        return $ret;
1117
+    }
1118
+
1119
+    /**
1120
+     * retrieved. Results will according to the order in the array.
1121
+     *
1122
+     * @param string $filter
1123
+     * @param string $base
1124
+     * @param string[] $attr
1125
+     * @param int|null $limit optional, maximum results to be counted
1126
+     * @param int|null $offset optional, a starting point
1127
+     * @return array|false array with the search result as first value and pagedSearchOK as
1128
+     * second | false if not successful
1129
+     * @throws ServerNotAvailableException
1130
+     */
1131
+    private function executeSearch(
1132
+        string $filter,
1133
+        string $base,
1134
+        ?array &$attr,
1135
+        ?int $limit,
1136
+        ?int $offset
1137
+    ) {
1138
+        // See if we have a resource, in case not cancel with message
1139
+        $cr = $this->connection->getConnectionResource();
1140
+        if (!$this->ldap->isResource($cr)) {
1141
+            // Seems like we didn't find any resource.
1142
+            // Return an empty array just like before.
1143
+            \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
1144
+            return false;
1145
+        }
1146
+
1147
+        //check whether paged search should be attempted
1148
+        $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, (int)$offset);
1149
+
1150
+        $sr = $this->invokeLDAPMethod('search', $cr, $base, $filter, $attr);
1151
+        // cannot use $cr anymore, might have changed in the previous call!
1152
+        $error = $this->ldap->errno($this->connection->getConnectionResource());
1153
+        if (!$this->ldap->isResource($sr) || $error !== 0) {
1154
+            \OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  ' . print_r($pagedSearchOK, true), ILogger::ERROR);
1155
+            return false;
1156
+        }
1157
+
1158
+        return [$sr, $pagedSearchOK];
1159
+    }
1160
+
1161
+    /**
1162
+     * processes an LDAP paged search operation
1163
+     *
1164
+     * @param resource $sr the array containing the LDAP search resources
1165
+     * @param int $foundItems number of results in the single search operation
1166
+     * @param int $limit maximum results to be counted
1167
+     * @param bool $pagedSearchOK whether a paged search has been executed
1168
+     * @param bool $skipHandling required for paged search when cookies to
1169
+     * prior results need to be gained
1170
+     * @return bool cookie validity, true if we have more pages, false otherwise.
1171
+     * @throws ServerNotAvailableException
1172
+     */
1173
+    private function processPagedSearchStatus(
1174
+        $sr,
1175
+        int $foundItems,
1176
+        int $limit,
1177
+        bool $pagedSearchOK,
1178
+        bool $skipHandling
1179
+    ): bool {
1180
+        $cookie = null;
1181
+        if ($pagedSearchOK) {
1182
+            $cr = $this->connection->getConnectionResource();
1183
+            if ($this->ldap->controlPagedResultResponse($cr, $sr, $cookie)) {
1184
+                $this->lastCookie = $cookie;
1185
+            }
1186
+
1187
+            //browsing through prior pages to get the cookie for the new one
1188
+            if ($skipHandling) {
1189
+                return false;
1190
+            }
1191
+            // if count is bigger, then the server does not support
1192
+            // paged search. Instead, he did a normal search. We set a
1193
+            // flag here, so the callee knows how to deal with it.
1194
+            if ($foundItems <= $limit) {
1195
+                $this->pagedSearchedSuccessful = true;
1196
+            }
1197
+        } else {
1198
+            if (!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1199
+                \OC::$server->getLogger()->debug(
1200
+                    'Paged search was not available',
1201
+                    ['app' => 'user_ldap']
1202
+                );
1203
+            }
1204
+        }
1205
+        /* ++ Fixing RHDS searches with pages with zero results ++
1206 1206
 		 * Return cookie status. If we don't have more pages, with RHDS
1207 1207
 		 * cookie is null, with openldap cookie is an empty string and
1208 1208
 		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1209 1209
 		 */
1210
-		return !empty($cookie) || $cookie === '0';
1211
-	}
1212
-
1213
-	/**
1214
-	 * executes an LDAP search, but counts the results only
1215
-	 *
1216
-	 * @param string $filter the LDAP filter for the search
1217
-	 * @param array $bases an array containing the LDAP subtree(s) that shall be searched
1218
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1219
-	 * retrieved. Results will according to the order in the array.
1220
-	 * @param int $limit optional, maximum results to be counted
1221
-	 * @param int $offset optional, a starting point
1222
-	 * @param bool $skipHandling indicates whether the pages search operation is
1223
-	 * completed
1224
-	 * @return int|false Integer or false if the search could not be initialized
1225
-	 * @throws ServerNotAvailableException
1226
-	 */
1227
-	private function count(
1228
-		string $filter,
1229
-		array $bases,
1230
-		$attr = null,
1231
-		?int $limit = null,
1232
-		?int $offset = null,
1233
-		bool $skipHandling = false
1234
-	) {
1235
-		\OC::$server->getLogger()->debug('Count filter: {filter}', [
1236
-			'app' => 'user_ldap',
1237
-			'filter' => $filter
1238
-		]);
1239
-
1240
-		if (!is_null($attr) && !is_array($attr)) {
1241
-			$attr = [mb_strtolower($attr, 'UTF-8')];
1242
-		}
1243
-
1244
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1245
-		if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1246
-			$limitPerPage = $limit;
1247
-		}
1248
-
1249
-		$counter = 0;
1250
-		$count = null;
1251
-		$this->connection->getConnectionResource();
1252
-
1253
-		foreach ($bases as $base) {
1254
-			do {
1255
-				$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1256
-				if ($search === false) {
1257
-					return $counter > 0 ? $counter : false;
1258
-				}
1259
-				list($sr, $pagedSearchOK) = $search;
1260
-
1261
-				/* ++ Fixing RHDS searches with pages with zero results ++
1210
+        return !empty($cookie) || $cookie === '0';
1211
+    }
1212
+
1213
+    /**
1214
+     * executes an LDAP search, but counts the results only
1215
+     *
1216
+     * @param string $filter the LDAP filter for the search
1217
+     * @param array $bases an array containing the LDAP subtree(s) that shall be searched
1218
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1219
+     * retrieved. Results will according to the order in the array.
1220
+     * @param int $limit optional, maximum results to be counted
1221
+     * @param int $offset optional, a starting point
1222
+     * @param bool $skipHandling indicates whether the pages search operation is
1223
+     * completed
1224
+     * @return int|false Integer or false if the search could not be initialized
1225
+     * @throws ServerNotAvailableException
1226
+     */
1227
+    private function count(
1228
+        string $filter,
1229
+        array $bases,
1230
+        $attr = null,
1231
+        ?int $limit = null,
1232
+        ?int $offset = null,
1233
+        bool $skipHandling = false
1234
+    ) {
1235
+        \OC::$server->getLogger()->debug('Count filter: {filter}', [
1236
+            'app' => 'user_ldap',
1237
+            'filter' => $filter
1238
+        ]);
1239
+
1240
+        if (!is_null($attr) && !is_array($attr)) {
1241
+            $attr = [mb_strtolower($attr, 'UTF-8')];
1242
+        }
1243
+
1244
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1245
+        if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1246
+            $limitPerPage = $limit;
1247
+        }
1248
+
1249
+        $counter = 0;
1250
+        $count = null;
1251
+        $this->connection->getConnectionResource();
1252
+
1253
+        foreach ($bases as $base) {
1254
+            do {
1255
+                $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1256
+                if ($search === false) {
1257
+                    return $counter > 0 ? $counter : false;
1258
+                }
1259
+                list($sr, $pagedSearchOK) = $search;
1260
+
1261
+                /* ++ Fixing RHDS searches with pages with zero results ++
1262 1262
 				 * countEntriesInSearchResults() method signature changed
1263 1263
 				 * by removing $limit and &$hasHitLimit parameters
1264 1264
 				 */
1265
-				$count = $this->countEntriesInSearchResults($sr);
1266
-				$counter += $count;
1265
+                $count = $this->countEntriesInSearchResults($sr);
1266
+                $counter += $count;
1267 1267
 
1268
-				$hasMorePages = $this->processPagedSearchStatus($sr, $count, $limitPerPage, $pagedSearchOK, $skipHandling);
1269
-				$offset += $limitPerPage;
1270
-				/* ++ Fixing RHDS searches with pages with zero results ++
1268
+                $hasMorePages = $this->processPagedSearchStatus($sr, $count, $limitPerPage, $pagedSearchOK, $skipHandling);
1269
+                $offset += $limitPerPage;
1270
+                /* ++ Fixing RHDS searches with pages with zero results ++
1271 1271
 				 * Continue now depends on $hasMorePages value
1272 1272
 				 */
1273
-				$continue = $pagedSearchOK && $hasMorePages;
1274
-			} while ($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1275
-		}
1276
-
1277
-		return $counter;
1278
-	}
1279
-
1280
-	/**
1281
-	 * @param resource $sr
1282
-	 * @return int
1283
-	 * @throws ServerNotAvailableException
1284
-	 */
1285
-	private function countEntriesInSearchResults($sr): int {
1286
-		return (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $sr);
1287
-	}
1288
-
1289
-	/**
1290
-	 * Executes an LDAP search
1291
-	 *
1292
-	 * @throws ServerNotAvailableException
1293
-	 */
1294
-	public function search(
1295
-		string $filter,
1296
-		string $base,
1297
-		?array $attr = null,
1298
-		?int $limit = null,
1299
-		?int $offset = null,
1300
-		bool $skipHandling = false
1301
-	): array {
1302
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1303
-		if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1304
-			$limitPerPage = $limit;
1305
-		}
1306
-
1307
-		if (!is_null($attr) && !is_array($attr)) {
1308
-			$attr = [mb_strtolower($attr, 'UTF-8')];
1309
-		}
1310
-
1311
-		/* ++ Fixing RHDS searches with pages with zero results ++
1273
+                $continue = $pagedSearchOK && $hasMorePages;
1274
+            } while ($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1275
+        }
1276
+
1277
+        return $counter;
1278
+    }
1279
+
1280
+    /**
1281
+     * @param resource $sr
1282
+     * @return int
1283
+     * @throws ServerNotAvailableException
1284
+     */
1285
+    private function countEntriesInSearchResults($sr): int {
1286
+        return (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $sr);
1287
+    }
1288
+
1289
+    /**
1290
+     * Executes an LDAP search
1291
+     *
1292
+     * @throws ServerNotAvailableException
1293
+     */
1294
+    public function search(
1295
+        string $filter,
1296
+        string $base,
1297
+        ?array $attr = null,
1298
+        ?int $limit = null,
1299
+        ?int $offset = null,
1300
+        bool $skipHandling = false
1301
+    ): array {
1302
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1303
+        if (!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1304
+            $limitPerPage = $limit;
1305
+        }
1306
+
1307
+        if (!is_null($attr) && !is_array($attr)) {
1308
+            $attr = [mb_strtolower($attr, 'UTF-8')];
1309
+        }
1310
+
1311
+        /* ++ Fixing RHDS searches with pages with zero results ++
1312 1312
 		 * As we can have pages with zero results and/or pages with less
1313 1313
 		 * than $limit results but with a still valid server 'cookie',
1314 1314
 		 * loops through until we get $continue equals true and
1315 1315
 		 * $findings['count'] < $limit
1316 1316
 		 */
1317
-		$findings = [];
1318
-		$savedoffset = $offset;
1319
-		$iFoundItems = 0;
1320
-
1321
-		do {
1322
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1323
-			if ($search === false) {
1324
-				return [];
1325
-			}
1326
-			list($sr, $pagedSearchOK) = $search;
1327
-			$cr = $this->connection->getConnectionResource();
1328
-
1329
-			if ($skipHandling) {
1330
-				//i.e. result do not need to be fetched, we just need the cookie
1331
-				//thus pass 1 or any other value as $iFoundItems because it is not
1332
-				//used
1333
-				$this->processPagedSearchStatus($sr, 1, $limitPerPage, $pagedSearchOK, $skipHandling);
1334
-				return [];
1335
-			}
1336
-
1337
-			$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $sr));
1338
-			$iFoundItems = max($iFoundItems, $findings['count']);
1339
-			unset($findings['count']);
1340
-
1341
-			$continue = $this->processPagedSearchStatus($sr, $iFoundItems, $limitPerPage, $pagedSearchOK, $skipHandling);
1342
-			$offset += $limitPerPage;
1343
-		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1344
-
1345
-		// resetting offset
1346
-		$offset = $savedoffset;
1347
-
1348
-		// if we're here, probably no connection resource is returned.
1349
-		// to make Nextcloud behave nicely, we simply give back an empty array.
1350
-		if (is_null($findings)) {
1351
-			return [];
1352
-		}
1353
-
1354
-		if (!is_null($attr)) {
1355
-			$selection = [];
1356
-			$i = 0;
1357
-			foreach ($findings as $item) {
1358
-				if (!is_array($item)) {
1359
-					continue;
1360
-				}
1361
-				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1362
-				foreach ($attr as $key) {
1363
-					if (isset($item[$key])) {
1364
-						if (is_array($item[$key]) && isset($item[$key]['count'])) {
1365
-							unset($item[$key]['count']);
1366
-						}
1367
-						if ($key !== 'dn') {
1368
-							if ($this->resemblesDN($key)) {
1369
-								$selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1370
-							} elseif ($key === 'objectguid' || $key === 'guid') {
1371
-								$selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1372
-							} else {
1373
-								$selection[$i][$key] = $item[$key];
1374
-							}
1375
-						} else {
1376
-							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1377
-						}
1378
-					}
1379
-				}
1380
-				$i++;
1381
-			}
1382
-			$findings = $selection;
1383
-		}
1384
-		//we slice the findings, when
1385
-		//a) paged search unsuccessful, though attempted
1386
-		//b) no paged search, but limit set
1387
-		if ((!$this->getPagedSearchResultState()
1388
-				&& $pagedSearchOK)
1389
-			|| (
1390
-				!$pagedSearchOK
1391
-				&& !is_null($limit)
1392
-			)
1393
-		) {
1394
-			$findings = array_slice($findings, (int)$offset, $limit);
1395
-		}
1396
-		return $findings;
1397
-	}
1398
-
1399
-	/**
1400
-	 * @param string $name
1401
-	 * @return string
1402
-	 * @throws \InvalidArgumentException
1403
-	 */
1404
-	public function sanitizeUsername($name) {
1405
-		$name = trim($name);
1406
-
1407
-		if ($this->connection->ldapIgnoreNamingRules) {
1408
-			return $name;
1409
-		}
1410
-
1411
-		// Transliteration to ASCII
1412
-		$transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1413
-		if ($transliterated !== false) {
1414
-			// depending on system config iconv can work or not
1415
-			$name = $transliterated;
1416
-		}
1417
-
1418
-		// Replacements
1419
-		$name = str_replace(' ', '_', $name);
1420
-
1421
-		// Every remaining disallowed characters will be removed
1422
-		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1423
-
1424
-		if ($name === '') {
1425
-			throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1426
-		}
1427
-
1428
-		return $name;
1429
-	}
1430
-
1431
-	/**
1432
-	 * escapes (user provided) parts for LDAP filter
1433
-	 *
1434
-	 * @param string $input , the provided value
1435
-	 * @param bool $allowAsterisk whether in * at the beginning should be preserved
1436
-	 * @return string the escaped string
1437
-	 */
1438
-	public function escapeFilterPart($input, $allowAsterisk = false): string {
1439
-		$asterisk = '';
1440
-		if ($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1441
-			$asterisk = '*';
1442
-			$input = mb_substr($input, 1, null, 'UTF-8');
1443
-		}
1444
-		$search = ['*', '\\', '(', ')'];
1445
-		$replace = ['\\*', '\\\\', '\\(', '\\)'];
1446
-		return $asterisk . str_replace($search, $replace, $input);
1447
-	}
1448
-
1449
-	/**
1450
-	 * combines the input filters with AND
1451
-	 *
1452
-	 * @param string[] $filters the filters to connect
1453
-	 * @return string the combined filter
1454
-	 */
1455
-	public function combineFilterWithAnd($filters): string {
1456
-		return $this->combineFilter($filters, '&');
1457
-	}
1458
-
1459
-	/**
1460
-	 * combines the input filters with OR
1461
-	 *
1462
-	 * @param string[] $filters the filters to connect
1463
-	 * @return string the combined filter
1464
-	 * Combines Filter arguments with OR
1465
-	 */
1466
-	public function combineFilterWithOr($filters) {
1467
-		return $this->combineFilter($filters, '|');
1468
-	}
1469
-
1470
-	/**
1471
-	 * combines the input filters with given operator
1472
-	 *
1473
-	 * @param string[] $filters the filters to connect
1474
-	 * @param string $operator either & or |
1475
-	 * @return string the combined filter
1476
-	 */
1477
-	private function combineFilter($filters, $operator) {
1478
-		$combinedFilter = '(' . $operator;
1479
-		foreach ($filters as $filter) {
1480
-			if ($filter !== '' && $filter[0] !== '(') {
1481
-				$filter = '(' . $filter . ')';
1482
-			}
1483
-			$combinedFilter .= $filter;
1484
-		}
1485
-		$combinedFilter .= ')';
1486
-		return $combinedFilter;
1487
-	}
1488
-
1489
-	/**
1490
-	 * creates a filter part for to perform search for users
1491
-	 *
1492
-	 * @param string $search the search term
1493
-	 * @return string the final filter part to use in LDAP searches
1494
-	 */
1495
-	public function getFilterPartForUserSearch($search) {
1496
-		return $this->getFilterPartForSearch($search,
1497
-			$this->connection->ldapAttributesForUserSearch,
1498
-			$this->connection->ldapUserDisplayName);
1499
-	}
1500
-
1501
-	/**
1502
-	 * creates a filter part for to perform search for groups
1503
-	 *
1504
-	 * @param string $search the search term
1505
-	 * @return string the final filter part to use in LDAP searches
1506
-	 */
1507
-	public function getFilterPartForGroupSearch($search) {
1508
-		return $this->getFilterPartForSearch($search,
1509
-			$this->connection->ldapAttributesForGroupSearch,
1510
-			$this->connection->ldapGroupDisplayName);
1511
-	}
1512
-
1513
-	/**
1514
-	 * creates a filter part for searches by splitting up the given search
1515
-	 * string into single words
1516
-	 *
1517
-	 * @param string $search the search term
1518
-	 * @param string[] $searchAttributes needs to have at least two attributes,
1519
-	 * otherwise it does not make sense :)
1520
-	 * @return string the final filter part to use in LDAP searches
1521
-	 * @throws \Exception
1522
-	 */
1523
-	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1524
-		if (!is_array($searchAttributes) || count($searchAttributes) < 2) {
1525
-			throw new \Exception('searchAttributes must be an array with at least two string');
1526
-		}
1527
-		$searchWords = explode(' ', trim($search));
1528
-		$wordFilters = [];
1529
-		foreach ($searchWords as $word) {
1530
-			$word = $this->prepareSearchTerm($word);
1531
-			//every word needs to appear at least once
1532
-			$wordMatchOneAttrFilters = [];
1533
-			foreach ($searchAttributes as $attr) {
1534
-				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1535
-			}
1536
-			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1537
-		}
1538
-		return $this->combineFilterWithAnd($wordFilters);
1539
-	}
1540
-
1541
-	/**
1542
-	 * creates a filter part for searches
1543
-	 *
1544
-	 * @param string $search the search term
1545
-	 * @param string[]|null $searchAttributes
1546
-	 * @param string $fallbackAttribute a fallback attribute in case the user
1547
-	 * did not define search attributes. Typically the display name attribute.
1548
-	 * @return string the final filter part to use in LDAP searches
1549
-	 */
1550
-	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1551
-		$filter = [];
1552
-		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1553
-		if ($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1554
-			try {
1555
-				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1556
-			} catch (\Exception $e) {
1557
-				\OCP\Util::writeLog(
1558
-					'user_ldap',
1559
-					'Creating advanced filter for search failed, falling back to simple method.',
1560
-					ILogger::INFO
1561
-				);
1562
-			}
1563
-		}
1564
-
1565
-		$search = $this->prepareSearchTerm($search);
1566
-		if (!is_array($searchAttributes) || count($searchAttributes) === 0) {
1567
-			if ($fallbackAttribute === '') {
1568
-				return '';
1569
-			}
1570
-			$filter[] = $fallbackAttribute . '=' . $search;
1571
-		} else {
1572
-			foreach ($searchAttributes as $attribute) {
1573
-				$filter[] = $attribute . '=' . $search;
1574
-			}
1575
-		}
1576
-		if (count($filter) === 1) {
1577
-			return '(' . $filter[0] . ')';
1578
-		}
1579
-		return $this->combineFilterWithOr($filter);
1580
-	}
1581
-
1582
-	/**
1583
-	 * returns the search term depending on whether we are allowed
1584
-	 * list users found by ldap with the current input appended by
1585
-	 * a *
1586
-	 *
1587
-	 * @return string
1588
-	 */
1589
-	private function prepareSearchTerm($term) {
1590
-		$config = \OC::$server->getConfig();
1591
-
1592
-		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1593
-
1594
-		$result = $term;
1595
-		if ($term === '') {
1596
-			$result = '*';
1597
-		} elseif ($allowEnum !== 'no') {
1598
-			$result = $term . '*';
1599
-		}
1600
-		return $result;
1601
-	}
1602
-
1603
-	/**
1604
-	 * returns the filter used for counting users
1605
-	 *
1606
-	 * @return string
1607
-	 */
1608
-	public function getFilterForUserCount() {
1609
-		$filter = $this->combineFilterWithAnd([
1610
-			$this->connection->ldapUserFilter,
1611
-			$this->connection->ldapUserDisplayName . '=*'
1612
-		]);
1613
-
1614
-		return $filter;
1615
-	}
1616
-
1617
-	/**
1618
-	 * @param string $name
1619
-	 * @param string $password
1620
-	 * @return bool
1621
-	 */
1622
-	public function areCredentialsValid($name, $password) {
1623
-		$name = $this->helper->DNasBaseParameter($name);
1624
-		$testConnection = clone $this->connection;
1625
-		$credentials = [
1626
-			'ldapAgentName' => $name,
1627
-			'ldapAgentPassword' => $password
1628
-		];
1629
-		if (!$testConnection->setConfiguration($credentials)) {
1630
-			return false;
1631
-		}
1632
-		return $testConnection->bind();
1633
-	}
1634
-
1635
-	/**
1636
-	 * reverse lookup of a DN given a known UUID
1637
-	 *
1638
-	 * @param string $uuid
1639
-	 * @return string
1640
-	 * @throws \Exception
1641
-	 */
1642
-	public function getUserDnByUuid($uuid) {
1643
-		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1644
-		$filter = $this->connection->ldapUserFilter;
1645
-		$bases = $this->connection->ldapBaseUsers;
1646
-
1647
-		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1648
-			// Sacrebleu! The UUID attribute is unknown :( We need first an
1649
-			// existing DN to be able to reliably detect it.
1650
-			foreach ($bases as $base) {
1651
-				$result = $this->search($filter, $base, ['dn'], 1);
1652
-				if (!isset($result[0]) || !isset($result[0]['dn'])) {
1653
-					continue;
1654
-				}
1655
-				$dn = $result[0]['dn'][0];
1656
-				if ($hasFound = $this->detectUuidAttribute($dn, true)) {
1657
-					break;
1658
-				}
1659
-			}
1660
-			if (!isset($hasFound) || !$hasFound) {
1661
-				throw new \Exception('Cannot determine UUID attribute');
1662
-			}
1663
-		} else {
1664
-			// The UUID attribute is either known or an override is given.
1665
-			// By calling this method we ensure that $this->connection->$uuidAttr
1666
-			// is definitely set
1667
-			if (!$this->detectUuidAttribute('', true)) {
1668
-				throw new \Exception('Cannot determine UUID attribute');
1669
-			}
1670
-		}
1671
-
1672
-		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1673
-		if ($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1674
-			$uuid = $this->formatGuid2ForFilterUser($uuid);
1675
-		}
1676
-
1677
-		$filter = $uuidAttr . '=' . $uuid;
1678
-		$result = $this->searchUsers($filter, ['dn'], 2);
1679
-		if (is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1680
-			// we put the count into account to make sure that this is
1681
-			// really unique
1682
-			return $result[0]['dn'][0];
1683
-		}
1684
-
1685
-		throw new \Exception('Cannot determine UUID attribute');
1686
-	}
1687
-
1688
-	/**
1689
-	 * auto-detects the directory's UUID attribute
1690
-	 *
1691
-	 * @param string $dn a known DN used to check against
1692
-	 * @param bool $isUser
1693
-	 * @param bool $force the detection should be run, even if it is not set to auto
1694
-	 * @param array|null $ldapRecord
1695
-	 * @return bool true on success, false otherwise
1696
-	 * @throws ServerNotAvailableException
1697
-	 */
1698
-	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1699
-		if ($isUser) {
1700
-			$uuidAttr = 'ldapUuidUserAttribute';
1701
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1702
-		} else {
1703
-			$uuidAttr = 'ldapUuidGroupAttribute';
1704
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1705
-		}
1706
-
1707
-		if (!$force) {
1708
-			if ($this->connection->$uuidAttr !== 'auto') {
1709
-				return true;
1710
-			} elseif (is_string($uuidOverride) && trim($uuidOverride) !== '') {
1711
-				$this->connection->$uuidAttr = $uuidOverride;
1712
-				return true;
1713
-			}
1714
-
1715
-			$attribute = $this->connection->getFromCache($uuidAttr);
1716
-			if (!$attribute === null) {
1717
-				$this->connection->$uuidAttr = $attribute;
1718
-				return true;
1719
-			}
1720
-		}
1721
-
1722
-		foreach (self::UUID_ATTRIBUTES as $attribute) {
1723
-			if ($ldapRecord !== null) {
1724
-				// we have the info from LDAP already, we don't need to talk to the server again
1725
-				if (isset($ldapRecord[$attribute])) {
1726
-					$this->connection->$uuidAttr = $attribute;
1727
-					return true;
1728
-				}
1729
-			}
1730
-
1731
-			$value = $this->readAttribute($dn, $attribute);
1732
-			if (is_array($value) && isset($value[0]) && !empty($value[0])) {
1733
-				\OC::$server->getLogger()->debug(
1734
-					'Setting {attribute} as {subject}',
1735
-					[
1736
-						'app' => 'user_ldap',
1737
-						'attribute' => $attribute,
1738
-						'subject' => $uuidAttr
1739
-					]
1740
-				);
1741
-				$this->connection->$uuidAttr = $attribute;
1742
-				$this->connection->writeToCache($uuidAttr, $attribute);
1743
-				return true;
1744
-			}
1745
-		}
1746
-		\OC::$server->getLogger()->debug('Could not autodetect the UUID attribute', ['app' => 'user_ldap']);
1747
-
1748
-		return false;
1749
-	}
1750
-
1751
-	/**
1752
-	 * @param string $dn
1753
-	 * @param bool $isUser
1754
-	 * @param null $ldapRecord
1755
-	 * @return bool|string
1756
-	 * @throws ServerNotAvailableException
1757
-	 */
1758
-	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1759
-		if ($isUser) {
1760
-			$uuidAttr = 'ldapUuidUserAttribute';
1761
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1762
-		} else {
1763
-			$uuidAttr = 'ldapUuidGroupAttribute';
1764
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1765
-		}
1766
-
1767
-		$uuid = false;
1768
-		if ($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1769
-			$attr = $this->connection->$uuidAttr;
1770
-			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1771
-			if (!is_array($uuid)
1772
-				&& $uuidOverride !== ''
1773
-				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord)) {
1774
-				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1775
-					? $ldapRecord[$this->connection->$uuidAttr]
1776
-					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1777
-			}
1778
-			if (is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1779
-				$uuid = $uuid[0];
1780
-			}
1781
-		}
1782
-
1783
-		return $uuid;
1784
-	}
1785
-
1786
-	/**
1787
-	 * converts a binary ObjectGUID into a string representation
1788
-	 *
1789
-	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1790
-	 * @return string
1791
-	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1792
-	 */
1793
-	private function convertObjectGUID2Str($oguid) {
1794
-		$hex_guid = bin2hex($oguid);
1795
-		$hex_guid_to_guid_str = '';
1796
-		for ($k = 1; $k <= 4; ++$k) {
1797
-			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1798
-		}
1799
-		$hex_guid_to_guid_str .= '-';
1800
-		for ($k = 1; $k <= 2; ++$k) {
1801
-			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1802
-		}
1803
-		$hex_guid_to_guid_str .= '-';
1804
-		for ($k = 1; $k <= 2; ++$k) {
1805
-			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1806
-		}
1807
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1808
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1809
-
1810
-		return strtoupper($hex_guid_to_guid_str);
1811
-	}
1812
-
1813
-	/**
1814
-	 * the first three blocks of the string-converted GUID happen to be in
1815
-	 * reverse order. In order to use it in a filter, this needs to be
1816
-	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1817
-	 * to every two hax figures.
1818
-	 *
1819
-	 * If an invalid string is passed, it will be returned without change.
1820
-	 *
1821
-	 * @param string $guid
1822
-	 * @return string
1823
-	 */
1824
-	public function formatGuid2ForFilterUser($guid) {
1825
-		if (!is_string($guid)) {
1826
-			throw new \InvalidArgumentException('String expected');
1827
-		}
1828
-		$blocks = explode('-', $guid);
1829
-		if (count($blocks) !== 5) {
1830
-			/*
1317
+        $findings = [];
1318
+        $savedoffset = $offset;
1319
+        $iFoundItems = 0;
1320
+
1321
+        do {
1322
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1323
+            if ($search === false) {
1324
+                return [];
1325
+            }
1326
+            list($sr, $pagedSearchOK) = $search;
1327
+            $cr = $this->connection->getConnectionResource();
1328
+
1329
+            if ($skipHandling) {
1330
+                //i.e. result do not need to be fetched, we just need the cookie
1331
+                //thus pass 1 or any other value as $iFoundItems because it is not
1332
+                //used
1333
+                $this->processPagedSearchStatus($sr, 1, $limitPerPage, $pagedSearchOK, $skipHandling);
1334
+                return [];
1335
+            }
1336
+
1337
+            $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $sr));
1338
+            $iFoundItems = max($iFoundItems, $findings['count']);
1339
+            unset($findings['count']);
1340
+
1341
+            $continue = $this->processPagedSearchStatus($sr, $iFoundItems, $limitPerPage, $pagedSearchOK, $skipHandling);
1342
+            $offset += $limitPerPage;
1343
+        } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1344
+
1345
+        // resetting offset
1346
+        $offset = $savedoffset;
1347
+
1348
+        // if we're here, probably no connection resource is returned.
1349
+        // to make Nextcloud behave nicely, we simply give back an empty array.
1350
+        if (is_null($findings)) {
1351
+            return [];
1352
+        }
1353
+
1354
+        if (!is_null($attr)) {
1355
+            $selection = [];
1356
+            $i = 0;
1357
+            foreach ($findings as $item) {
1358
+                if (!is_array($item)) {
1359
+                    continue;
1360
+                }
1361
+                $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1362
+                foreach ($attr as $key) {
1363
+                    if (isset($item[$key])) {
1364
+                        if (is_array($item[$key]) && isset($item[$key]['count'])) {
1365
+                            unset($item[$key]['count']);
1366
+                        }
1367
+                        if ($key !== 'dn') {
1368
+                            if ($this->resemblesDN($key)) {
1369
+                                $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1370
+                            } elseif ($key === 'objectguid' || $key === 'guid') {
1371
+                                $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1372
+                            } else {
1373
+                                $selection[$i][$key] = $item[$key];
1374
+                            }
1375
+                        } else {
1376
+                            $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1377
+                        }
1378
+                    }
1379
+                }
1380
+                $i++;
1381
+            }
1382
+            $findings = $selection;
1383
+        }
1384
+        //we slice the findings, when
1385
+        //a) paged search unsuccessful, though attempted
1386
+        //b) no paged search, but limit set
1387
+        if ((!$this->getPagedSearchResultState()
1388
+                && $pagedSearchOK)
1389
+            || (
1390
+                !$pagedSearchOK
1391
+                && !is_null($limit)
1392
+            )
1393
+        ) {
1394
+            $findings = array_slice($findings, (int)$offset, $limit);
1395
+        }
1396
+        return $findings;
1397
+    }
1398
+
1399
+    /**
1400
+     * @param string $name
1401
+     * @return string
1402
+     * @throws \InvalidArgumentException
1403
+     */
1404
+    public function sanitizeUsername($name) {
1405
+        $name = trim($name);
1406
+
1407
+        if ($this->connection->ldapIgnoreNamingRules) {
1408
+            return $name;
1409
+        }
1410
+
1411
+        // Transliteration to ASCII
1412
+        $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1413
+        if ($transliterated !== false) {
1414
+            // depending on system config iconv can work or not
1415
+            $name = $transliterated;
1416
+        }
1417
+
1418
+        // Replacements
1419
+        $name = str_replace(' ', '_', $name);
1420
+
1421
+        // Every remaining disallowed characters will be removed
1422
+        $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1423
+
1424
+        if ($name === '') {
1425
+            throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1426
+        }
1427
+
1428
+        return $name;
1429
+    }
1430
+
1431
+    /**
1432
+     * escapes (user provided) parts for LDAP filter
1433
+     *
1434
+     * @param string $input , the provided value
1435
+     * @param bool $allowAsterisk whether in * at the beginning should be preserved
1436
+     * @return string the escaped string
1437
+     */
1438
+    public function escapeFilterPart($input, $allowAsterisk = false): string {
1439
+        $asterisk = '';
1440
+        if ($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1441
+            $asterisk = '*';
1442
+            $input = mb_substr($input, 1, null, 'UTF-8');
1443
+        }
1444
+        $search = ['*', '\\', '(', ')'];
1445
+        $replace = ['\\*', '\\\\', '\\(', '\\)'];
1446
+        return $asterisk . str_replace($search, $replace, $input);
1447
+    }
1448
+
1449
+    /**
1450
+     * combines the input filters with AND
1451
+     *
1452
+     * @param string[] $filters the filters to connect
1453
+     * @return string the combined filter
1454
+     */
1455
+    public function combineFilterWithAnd($filters): string {
1456
+        return $this->combineFilter($filters, '&');
1457
+    }
1458
+
1459
+    /**
1460
+     * combines the input filters with OR
1461
+     *
1462
+     * @param string[] $filters the filters to connect
1463
+     * @return string the combined filter
1464
+     * Combines Filter arguments with OR
1465
+     */
1466
+    public function combineFilterWithOr($filters) {
1467
+        return $this->combineFilter($filters, '|');
1468
+    }
1469
+
1470
+    /**
1471
+     * combines the input filters with given operator
1472
+     *
1473
+     * @param string[] $filters the filters to connect
1474
+     * @param string $operator either & or |
1475
+     * @return string the combined filter
1476
+     */
1477
+    private function combineFilter($filters, $operator) {
1478
+        $combinedFilter = '(' . $operator;
1479
+        foreach ($filters as $filter) {
1480
+            if ($filter !== '' && $filter[0] !== '(') {
1481
+                $filter = '(' . $filter . ')';
1482
+            }
1483
+            $combinedFilter .= $filter;
1484
+        }
1485
+        $combinedFilter .= ')';
1486
+        return $combinedFilter;
1487
+    }
1488
+
1489
+    /**
1490
+     * creates a filter part for to perform search for users
1491
+     *
1492
+     * @param string $search the search term
1493
+     * @return string the final filter part to use in LDAP searches
1494
+     */
1495
+    public function getFilterPartForUserSearch($search) {
1496
+        return $this->getFilterPartForSearch($search,
1497
+            $this->connection->ldapAttributesForUserSearch,
1498
+            $this->connection->ldapUserDisplayName);
1499
+    }
1500
+
1501
+    /**
1502
+     * creates a filter part for to perform search for groups
1503
+     *
1504
+     * @param string $search the search term
1505
+     * @return string the final filter part to use in LDAP searches
1506
+     */
1507
+    public function getFilterPartForGroupSearch($search) {
1508
+        return $this->getFilterPartForSearch($search,
1509
+            $this->connection->ldapAttributesForGroupSearch,
1510
+            $this->connection->ldapGroupDisplayName);
1511
+    }
1512
+
1513
+    /**
1514
+     * creates a filter part for searches by splitting up the given search
1515
+     * string into single words
1516
+     *
1517
+     * @param string $search the search term
1518
+     * @param string[] $searchAttributes needs to have at least two attributes,
1519
+     * otherwise it does not make sense :)
1520
+     * @return string the final filter part to use in LDAP searches
1521
+     * @throws \Exception
1522
+     */
1523
+    private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1524
+        if (!is_array($searchAttributes) || count($searchAttributes) < 2) {
1525
+            throw new \Exception('searchAttributes must be an array with at least two string');
1526
+        }
1527
+        $searchWords = explode(' ', trim($search));
1528
+        $wordFilters = [];
1529
+        foreach ($searchWords as $word) {
1530
+            $word = $this->prepareSearchTerm($word);
1531
+            //every word needs to appear at least once
1532
+            $wordMatchOneAttrFilters = [];
1533
+            foreach ($searchAttributes as $attr) {
1534
+                $wordMatchOneAttrFilters[] = $attr . '=' . $word;
1535
+            }
1536
+            $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1537
+        }
1538
+        return $this->combineFilterWithAnd($wordFilters);
1539
+    }
1540
+
1541
+    /**
1542
+     * creates a filter part for searches
1543
+     *
1544
+     * @param string $search the search term
1545
+     * @param string[]|null $searchAttributes
1546
+     * @param string $fallbackAttribute a fallback attribute in case the user
1547
+     * did not define search attributes. Typically the display name attribute.
1548
+     * @return string the final filter part to use in LDAP searches
1549
+     */
1550
+    private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1551
+        $filter = [];
1552
+        $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1553
+        if ($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1554
+            try {
1555
+                return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1556
+            } catch (\Exception $e) {
1557
+                \OCP\Util::writeLog(
1558
+                    'user_ldap',
1559
+                    'Creating advanced filter for search failed, falling back to simple method.',
1560
+                    ILogger::INFO
1561
+                );
1562
+            }
1563
+        }
1564
+
1565
+        $search = $this->prepareSearchTerm($search);
1566
+        if (!is_array($searchAttributes) || count($searchAttributes) === 0) {
1567
+            if ($fallbackAttribute === '') {
1568
+                return '';
1569
+            }
1570
+            $filter[] = $fallbackAttribute . '=' . $search;
1571
+        } else {
1572
+            foreach ($searchAttributes as $attribute) {
1573
+                $filter[] = $attribute . '=' . $search;
1574
+            }
1575
+        }
1576
+        if (count($filter) === 1) {
1577
+            return '(' . $filter[0] . ')';
1578
+        }
1579
+        return $this->combineFilterWithOr($filter);
1580
+    }
1581
+
1582
+    /**
1583
+     * returns the search term depending on whether we are allowed
1584
+     * list users found by ldap with the current input appended by
1585
+     * a *
1586
+     *
1587
+     * @return string
1588
+     */
1589
+    private function prepareSearchTerm($term) {
1590
+        $config = \OC::$server->getConfig();
1591
+
1592
+        $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1593
+
1594
+        $result = $term;
1595
+        if ($term === '') {
1596
+            $result = '*';
1597
+        } elseif ($allowEnum !== 'no') {
1598
+            $result = $term . '*';
1599
+        }
1600
+        return $result;
1601
+    }
1602
+
1603
+    /**
1604
+     * returns the filter used for counting users
1605
+     *
1606
+     * @return string
1607
+     */
1608
+    public function getFilterForUserCount() {
1609
+        $filter = $this->combineFilterWithAnd([
1610
+            $this->connection->ldapUserFilter,
1611
+            $this->connection->ldapUserDisplayName . '=*'
1612
+        ]);
1613
+
1614
+        return $filter;
1615
+    }
1616
+
1617
+    /**
1618
+     * @param string $name
1619
+     * @param string $password
1620
+     * @return bool
1621
+     */
1622
+    public function areCredentialsValid($name, $password) {
1623
+        $name = $this->helper->DNasBaseParameter($name);
1624
+        $testConnection = clone $this->connection;
1625
+        $credentials = [
1626
+            'ldapAgentName' => $name,
1627
+            'ldapAgentPassword' => $password
1628
+        ];
1629
+        if (!$testConnection->setConfiguration($credentials)) {
1630
+            return false;
1631
+        }
1632
+        return $testConnection->bind();
1633
+    }
1634
+
1635
+    /**
1636
+     * reverse lookup of a DN given a known UUID
1637
+     *
1638
+     * @param string $uuid
1639
+     * @return string
1640
+     * @throws \Exception
1641
+     */
1642
+    public function getUserDnByUuid($uuid) {
1643
+        $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1644
+        $filter = $this->connection->ldapUserFilter;
1645
+        $bases = $this->connection->ldapBaseUsers;
1646
+
1647
+        if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1648
+            // Sacrebleu! The UUID attribute is unknown :( We need first an
1649
+            // existing DN to be able to reliably detect it.
1650
+            foreach ($bases as $base) {
1651
+                $result = $this->search($filter, $base, ['dn'], 1);
1652
+                if (!isset($result[0]) || !isset($result[0]['dn'])) {
1653
+                    continue;
1654
+                }
1655
+                $dn = $result[0]['dn'][0];
1656
+                if ($hasFound = $this->detectUuidAttribute($dn, true)) {
1657
+                    break;
1658
+                }
1659
+            }
1660
+            if (!isset($hasFound) || !$hasFound) {
1661
+                throw new \Exception('Cannot determine UUID attribute');
1662
+            }
1663
+        } else {
1664
+            // The UUID attribute is either known or an override is given.
1665
+            // By calling this method we ensure that $this->connection->$uuidAttr
1666
+            // is definitely set
1667
+            if (!$this->detectUuidAttribute('', true)) {
1668
+                throw new \Exception('Cannot determine UUID attribute');
1669
+            }
1670
+        }
1671
+
1672
+        $uuidAttr = $this->connection->ldapUuidUserAttribute;
1673
+        if ($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1674
+            $uuid = $this->formatGuid2ForFilterUser($uuid);
1675
+        }
1676
+
1677
+        $filter = $uuidAttr . '=' . $uuid;
1678
+        $result = $this->searchUsers($filter, ['dn'], 2);
1679
+        if (is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1680
+            // we put the count into account to make sure that this is
1681
+            // really unique
1682
+            return $result[0]['dn'][0];
1683
+        }
1684
+
1685
+        throw new \Exception('Cannot determine UUID attribute');
1686
+    }
1687
+
1688
+    /**
1689
+     * auto-detects the directory's UUID attribute
1690
+     *
1691
+     * @param string $dn a known DN used to check against
1692
+     * @param bool $isUser
1693
+     * @param bool $force the detection should be run, even if it is not set to auto
1694
+     * @param array|null $ldapRecord
1695
+     * @return bool true on success, false otherwise
1696
+     * @throws ServerNotAvailableException
1697
+     */
1698
+    private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1699
+        if ($isUser) {
1700
+            $uuidAttr = 'ldapUuidUserAttribute';
1701
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1702
+        } else {
1703
+            $uuidAttr = 'ldapUuidGroupAttribute';
1704
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1705
+        }
1706
+
1707
+        if (!$force) {
1708
+            if ($this->connection->$uuidAttr !== 'auto') {
1709
+                return true;
1710
+            } elseif (is_string($uuidOverride) && trim($uuidOverride) !== '') {
1711
+                $this->connection->$uuidAttr = $uuidOverride;
1712
+                return true;
1713
+            }
1714
+
1715
+            $attribute = $this->connection->getFromCache($uuidAttr);
1716
+            if (!$attribute === null) {
1717
+                $this->connection->$uuidAttr = $attribute;
1718
+                return true;
1719
+            }
1720
+        }
1721
+
1722
+        foreach (self::UUID_ATTRIBUTES as $attribute) {
1723
+            if ($ldapRecord !== null) {
1724
+                // we have the info from LDAP already, we don't need to talk to the server again
1725
+                if (isset($ldapRecord[$attribute])) {
1726
+                    $this->connection->$uuidAttr = $attribute;
1727
+                    return true;
1728
+                }
1729
+            }
1730
+
1731
+            $value = $this->readAttribute($dn, $attribute);
1732
+            if (is_array($value) && isset($value[0]) && !empty($value[0])) {
1733
+                \OC::$server->getLogger()->debug(
1734
+                    'Setting {attribute} as {subject}',
1735
+                    [
1736
+                        'app' => 'user_ldap',
1737
+                        'attribute' => $attribute,
1738
+                        'subject' => $uuidAttr
1739
+                    ]
1740
+                );
1741
+                $this->connection->$uuidAttr = $attribute;
1742
+                $this->connection->writeToCache($uuidAttr, $attribute);
1743
+                return true;
1744
+            }
1745
+        }
1746
+        \OC::$server->getLogger()->debug('Could not autodetect the UUID attribute', ['app' => 'user_ldap']);
1747
+
1748
+        return false;
1749
+    }
1750
+
1751
+    /**
1752
+     * @param string $dn
1753
+     * @param bool $isUser
1754
+     * @param null $ldapRecord
1755
+     * @return bool|string
1756
+     * @throws ServerNotAvailableException
1757
+     */
1758
+    public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1759
+        if ($isUser) {
1760
+            $uuidAttr = 'ldapUuidUserAttribute';
1761
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1762
+        } else {
1763
+            $uuidAttr = 'ldapUuidGroupAttribute';
1764
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1765
+        }
1766
+
1767
+        $uuid = false;
1768
+        if ($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1769
+            $attr = $this->connection->$uuidAttr;
1770
+            $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1771
+            if (!is_array($uuid)
1772
+                && $uuidOverride !== ''
1773
+                && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord)) {
1774
+                $uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1775
+                    ? $ldapRecord[$this->connection->$uuidAttr]
1776
+                    : $this->readAttribute($dn, $this->connection->$uuidAttr);
1777
+            }
1778
+            if (is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1779
+                $uuid = $uuid[0];
1780
+            }
1781
+        }
1782
+
1783
+        return $uuid;
1784
+    }
1785
+
1786
+    /**
1787
+     * converts a binary ObjectGUID into a string representation
1788
+     *
1789
+     * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1790
+     * @return string
1791
+     * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1792
+     */
1793
+    private function convertObjectGUID2Str($oguid) {
1794
+        $hex_guid = bin2hex($oguid);
1795
+        $hex_guid_to_guid_str = '';
1796
+        for ($k = 1; $k <= 4; ++$k) {
1797
+            $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1798
+        }
1799
+        $hex_guid_to_guid_str .= '-';
1800
+        for ($k = 1; $k <= 2; ++$k) {
1801
+            $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1802
+        }
1803
+        $hex_guid_to_guid_str .= '-';
1804
+        for ($k = 1; $k <= 2; ++$k) {
1805
+            $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1806
+        }
1807
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1808
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1809
+
1810
+        return strtoupper($hex_guid_to_guid_str);
1811
+    }
1812
+
1813
+    /**
1814
+     * the first three blocks of the string-converted GUID happen to be in
1815
+     * reverse order. In order to use it in a filter, this needs to be
1816
+     * corrected. Furthermore the dashes need to be replaced and \\ preprended
1817
+     * to every two hax figures.
1818
+     *
1819
+     * If an invalid string is passed, it will be returned without change.
1820
+     *
1821
+     * @param string $guid
1822
+     * @return string
1823
+     */
1824
+    public function formatGuid2ForFilterUser($guid) {
1825
+        if (!is_string($guid)) {
1826
+            throw new \InvalidArgumentException('String expected');
1827
+        }
1828
+        $blocks = explode('-', $guid);
1829
+        if (count($blocks) !== 5) {
1830
+            /*
1831 1831
 			 * Why not throw an Exception instead? This method is a utility
1832 1832
 			 * called only when trying to figure out whether a "missing" known
1833 1833
 			 * LDAP user was or was not renamed on the LDAP server. And this
@@ -1838,246 +1838,246 @@  discard block
 block discarded – undo
1838 1838
 			 * an exception here would kill the experience for a valid, acting
1839 1839
 			 * user. Instead we write a log message.
1840 1840
 			 */
1841
-			\OC::$server->getLogger()->info(
1842
-				'Passed string does not resemble a valid GUID. Known UUID ' .
1843
-				'({uuid}) probably does not match UUID configuration.',
1844
-				['app' => 'user_ldap', 'uuid' => $guid]
1845
-			);
1846
-			return $guid;
1847
-		}
1848
-		for ($i = 0; $i < 3; $i++) {
1849
-			$pairs = str_split($blocks[$i], 2);
1850
-			$pairs = array_reverse($pairs);
1851
-			$blocks[$i] = implode('', $pairs);
1852
-		}
1853
-		for ($i = 0; $i < 5; $i++) {
1854
-			$pairs = str_split($blocks[$i], 2);
1855
-			$blocks[$i] = '\\' . implode('\\', $pairs);
1856
-		}
1857
-		return implode('', $blocks);
1858
-	}
1859
-
1860
-	/**
1861
-	 * gets a SID of the domain of the given dn
1862
-	 *
1863
-	 * @param string $dn
1864
-	 * @return string|bool
1865
-	 * @throws ServerNotAvailableException
1866
-	 */
1867
-	public function getSID($dn) {
1868
-		$domainDN = $this->getDomainDNFromDN($dn);
1869
-		$cacheKey = 'getSID-' . $domainDN;
1870
-		$sid = $this->connection->getFromCache($cacheKey);
1871
-		if (!is_null($sid)) {
1872
-			return $sid;
1873
-		}
1874
-
1875
-		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1876
-		if (!is_array($objectSid) || empty($objectSid)) {
1877
-			$this->connection->writeToCache($cacheKey, false);
1878
-			return false;
1879
-		}
1880
-		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1881
-		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1882
-
1883
-		return $domainObjectSid;
1884
-	}
1885
-
1886
-	/**
1887
-	 * converts a binary SID into a string representation
1888
-	 *
1889
-	 * @param string $sid
1890
-	 * @return string
1891
-	 */
1892
-	public function convertSID2Str($sid) {
1893
-		// The format of a SID binary string is as follows:
1894
-		// 1 byte for the revision level
1895
-		// 1 byte for the number n of variable sub-ids
1896
-		// 6 bytes for identifier authority value
1897
-		// n*4 bytes for n sub-ids
1898
-		//
1899
-		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1900
-		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1901
-		$revision = ord($sid[0]);
1902
-		$numberSubID = ord($sid[1]);
1903
-
1904
-		$subIdStart = 8; // 1 + 1 + 6
1905
-		$subIdLength = 4;
1906
-		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1907
-			// Incorrect number of bytes present.
1908
-			return '';
1909
-		}
1910
-
1911
-		// 6 bytes = 48 bits can be represented using floats without loss of
1912
-		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1913
-		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1914
-
1915
-		$subIDs = [];
1916
-		for ($i = 0; $i < $numberSubID; $i++) {
1917
-			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1918
-			$subIDs[] = sprintf('%u', $subID[1]);
1919
-		}
1920
-
1921
-		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1922
-		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1923
-	}
1924
-
1925
-	/**
1926
-	 * checks if the given DN is part of the given base DN(s)
1927
-	 *
1928
-	 * @param string $dn the DN
1929
-	 * @param string[] $bases array containing the allowed base DN or DNs
1930
-	 * @return bool
1931
-	 */
1932
-	public function isDNPartOfBase($dn, $bases) {
1933
-		$belongsToBase = false;
1934
-		$bases = $this->helper->sanitizeDN($bases);
1935
-
1936
-		foreach ($bases as $base) {
1937
-			$belongsToBase = true;
1938
-			if (mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8') - mb_strlen($base, 'UTF-8'))) {
1939
-				$belongsToBase = false;
1940
-			}
1941
-			if ($belongsToBase) {
1942
-				break;
1943
-			}
1944
-		}
1945
-		return $belongsToBase;
1946
-	}
1947
-
1948
-	/**
1949
-	 * resets a running Paged Search operation
1950
-	 *
1951
-	 * @throws ServerNotAvailableException
1952
-	 */
1953
-	private function abandonPagedSearch() {
1954
-		if ($this->lastCookie === '') {
1955
-			return;
1956
-		}
1957
-		$cr = $this->connection->getConnectionResource();
1958
-		$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false);
1959
-		$this->getPagedSearchResultState();
1960
-		$this->lastCookie = '';
1961
-	}
1962
-
1963
-	/**
1964
-	 * checks whether an LDAP paged search operation has more pages that can be
1965
-	 * retrieved, typically when offset and limit are provided.
1966
-	 *
1967
-	 * Be very careful to use it: the last cookie value, which is inspected, can
1968
-	 * be reset by other operations. Best, call it immediately after a search(),
1969
-	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1970
-	 * well. Don't rely on it with any fetchList-method.
1971
-	 *
1972
-	 * @return bool
1973
-	 */
1974
-	public function hasMoreResults() {
1975
-		if (empty($this->lastCookie) && $this->lastCookie !== '0') {
1976
-			// as in RFC 2696, when all results are returned, the cookie will
1977
-			// be empty.
1978
-			return false;
1979
-		}
1980
-
1981
-		return true;
1982
-	}
1983
-
1984
-	/**
1985
-	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1986
-	 *
1987
-	 * @return boolean|null true on success, null or false otherwise
1988
-	 */
1989
-	public function getPagedSearchResultState() {
1990
-		$result = $this->pagedSearchedSuccessful;
1991
-		$this->pagedSearchedSuccessful = null;
1992
-		return $result;
1993
-	}
1994
-
1995
-	/**
1996
-	 * Prepares a paged search, if possible
1997
-	 *
1998
-	 * @param string $filter the LDAP filter for the search
1999
-	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
2000
-	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
2001
-	 * @param int $limit
2002
-	 * @param int $offset
2003
-	 * @return bool|true
2004
-	 * @throws ServerNotAvailableException
2005
-	 */
2006
-	private function initPagedSearch(
2007
-		string $filter,
2008
-		string $base,
2009
-		?array $attr,
2010
-		int $limit,
2011
-		int $offset
2012
-	): bool {
2013
-		$pagedSearchOK = false;
2014
-		if ($limit !== 0) {
2015
-			\OC::$server->getLogger()->debug(
2016
-				'initializing paged search for filter {filter}, base {base}, attr {attr}, limit {limit}, offset {offset}',
2017
-				[
2018
-					'app' => 'user_ldap',
2019
-					'filter' => $filter,
2020
-					'base' => $base,
2021
-					'attr' => $attr,
2022
-					'limit' => $limit,
2023
-					'offset' => $offset
2024
-				]
2025
-			);
2026
-			//get the cookie from the search for the previous search, required by LDAP
2027
-			if (empty($this->lastCookie) && $this->lastCookie !== "0" && ($offset > 0)) {
2028
-				// no cookie known from a potential previous search. We need
2029
-				// to start from 0 to come to the desired page. cookie value
2030
-				// of '0' is valid, because 389ds
2031
-				$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
2032
-				$this->search($filter, $base, $attr, $limit, $reOffset, true);
2033
-				if (!$this->hasMoreResults()) {
2034
-					// when the cookie is reset with != 0 offset, there are no further
2035
-					// results, so stop.
2036
-					return false;
2037
-				}
2038
-			}
2039
-			if ($this->lastCookie !== '' && $offset === 0) {
2040
-				//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
2041
-				$this->abandonPagedSearch();
2042
-			}
2043
-			$pagedSearchOK = true === $this->invokeLDAPMethod(
2044
-					'controlPagedResult', $this->connection->getConnectionResource(), $limit, false
2045
-				);
2046
-			if ($pagedSearchOK) {
2047
-				\OC::$server->getLogger()->debug('Ready for a paged search', ['app' => 'user_ldap']);
2048
-			}
2049
-			/* ++ Fixing RHDS searches with pages with zero results ++
1841
+            \OC::$server->getLogger()->info(
1842
+                'Passed string does not resemble a valid GUID. Known UUID ' .
1843
+                '({uuid}) probably does not match UUID configuration.',
1844
+                ['app' => 'user_ldap', 'uuid' => $guid]
1845
+            );
1846
+            return $guid;
1847
+        }
1848
+        for ($i = 0; $i < 3; $i++) {
1849
+            $pairs = str_split($blocks[$i], 2);
1850
+            $pairs = array_reverse($pairs);
1851
+            $blocks[$i] = implode('', $pairs);
1852
+        }
1853
+        for ($i = 0; $i < 5; $i++) {
1854
+            $pairs = str_split($blocks[$i], 2);
1855
+            $blocks[$i] = '\\' . implode('\\', $pairs);
1856
+        }
1857
+        return implode('', $blocks);
1858
+    }
1859
+
1860
+    /**
1861
+     * gets a SID of the domain of the given dn
1862
+     *
1863
+     * @param string $dn
1864
+     * @return string|bool
1865
+     * @throws ServerNotAvailableException
1866
+     */
1867
+    public function getSID($dn) {
1868
+        $domainDN = $this->getDomainDNFromDN($dn);
1869
+        $cacheKey = 'getSID-' . $domainDN;
1870
+        $sid = $this->connection->getFromCache($cacheKey);
1871
+        if (!is_null($sid)) {
1872
+            return $sid;
1873
+        }
1874
+
1875
+        $objectSid = $this->readAttribute($domainDN, 'objectsid');
1876
+        if (!is_array($objectSid) || empty($objectSid)) {
1877
+            $this->connection->writeToCache($cacheKey, false);
1878
+            return false;
1879
+        }
1880
+        $domainObjectSid = $this->convertSID2Str($objectSid[0]);
1881
+        $this->connection->writeToCache($cacheKey, $domainObjectSid);
1882
+
1883
+        return $domainObjectSid;
1884
+    }
1885
+
1886
+    /**
1887
+     * converts a binary SID into a string representation
1888
+     *
1889
+     * @param string $sid
1890
+     * @return string
1891
+     */
1892
+    public function convertSID2Str($sid) {
1893
+        // The format of a SID binary string is as follows:
1894
+        // 1 byte for the revision level
1895
+        // 1 byte for the number n of variable sub-ids
1896
+        // 6 bytes for identifier authority value
1897
+        // n*4 bytes for n sub-ids
1898
+        //
1899
+        // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1900
+        //  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1901
+        $revision = ord($sid[0]);
1902
+        $numberSubID = ord($sid[1]);
1903
+
1904
+        $subIdStart = 8; // 1 + 1 + 6
1905
+        $subIdLength = 4;
1906
+        if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1907
+            // Incorrect number of bytes present.
1908
+            return '';
1909
+        }
1910
+
1911
+        // 6 bytes = 48 bits can be represented using floats without loss of
1912
+        // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1913
+        $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1914
+
1915
+        $subIDs = [];
1916
+        for ($i = 0; $i < $numberSubID; $i++) {
1917
+            $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1918
+            $subIDs[] = sprintf('%u', $subID[1]);
1919
+        }
1920
+
1921
+        // Result for example above: S-1-5-21-249921958-728525901-1594176202
1922
+        return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1923
+    }
1924
+
1925
+    /**
1926
+     * checks if the given DN is part of the given base DN(s)
1927
+     *
1928
+     * @param string $dn the DN
1929
+     * @param string[] $bases array containing the allowed base DN or DNs
1930
+     * @return bool
1931
+     */
1932
+    public function isDNPartOfBase($dn, $bases) {
1933
+        $belongsToBase = false;
1934
+        $bases = $this->helper->sanitizeDN($bases);
1935
+
1936
+        foreach ($bases as $base) {
1937
+            $belongsToBase = true;
1938
+            if (mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8') - mb_strlen($base, 'UTF-8'))) {
1939
+                $belongsToBase = false;
1940
+            }
1941
+            if ($belongsToBase) {
1942
+                break;
1943
+            }
1944
+        }
1945
+        return $belongsToBase;
1946
+    }
1947
+
1948
+    /**
1949
+     * resets a running Paged Search operation
1950
+     *
1951
+     * @throws ServerNotAvailableException
1952
+     */
1953
+    private function abandonPagedSearch() {
1954
+        if ($this->lastCookie === '') {
1955
+            return;
1956
+        }
1957
+        $cr = $this->connection->getConnectionResource();
1958
+        $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false);
1959
+        $this->getPagedSearchResultState();
1960
+        $this->lastCookie = '';
1961
+    }
1962
+
1963
+    /**
1964
+     * checks whether an LDAP paged search operation has more pages that can be
1965
+     * retrieved, typically when offset and limit are provided.
1966
+     *
1967
+     * Be very careful to use it: the last cookie value, which is inspected, can
1968
+     * be reset by other operations. Best, call it immediately after a search(),
1969
+     * searchUsers() or searchGroups() call. count-methods are probably safe as
1970
+     * well. Don't rely on it with any fetchList-method.
1971
+     *
1972
+     * @return bool
1973
+     */
1974
+    public function hasMoreResults() {
1975
+        if (empty($this->lastCookie) && $this->lastCookie !== '0') {
1976
+            // as in RFC 2696, when all results are returned, the cookie will
1977
+            // be empty.
1978
+            return false;
1979
+        }
1980
+
1981
+        return true;
1982
+    }
1983
+
1984
+    /**
1985
+     * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1986
+     *
1987
+     * @return boolean|null true on success, null or false otherwise
1988
+     */
1989
+    public function getPagedSearchResultState() {
1990
+        $result = $this->pagedSearchedSuccessful;
1991
+        $this->pagedSearchedSuccessful = null;
1992
+        return $result;
1993
+    }
1994
+
1995
+    /**
1996
+     * Prepares a paged search, if possible
1997
+     *
1998
+     * @param string $filter the LDAP filter for the search
1999
+     * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
2000
+     * @param string[] $attr optional, when a certain attribute shall be filtered outside
2001
+     * @param int $limit
2002
+     * @param int $offset
2003
+     * @return bool|true
2004
+     * @throws ServerNotAvailableException
2005
+     */
2006
+    private function initPagedSearch(
2007
+        string $filter,
2008
+        string $base,
2009
+        ?array $attr,
2010
+        int $limit,
2011
+        int $offset
2012
+    ): bool {
2013
+        $pagedSearchOK = false;
2014
+        if ($limit !== 0) {
2015
+            \OC::$server->getLogger()->debug(
2016
+                'initializing paged search for filter {filter}, base {base}, attr {attr}, limit {limit}, offset {offset}',
2017
+                [
2018
+                    'app' => 'user_ldap',
2019
+                    'filter' => $filter,
2020
+                    'base' => $base,
2021
+                    'attr' => $attr,
2022
+                    'limit' => $limit,
2023
+                    'offset' => $offset
2024
+                ]
2025
+            );
2026
+            //get the cookie from the search for the previous search, required by LDAP
2027
+            if (empty($this->lastCookie) && $this->lastCookie !== "0" && ($offset > 0)) {
2028
+                // no cookie known from a potential previous search. We need
2029
+                // to start from 0 to come to the desired page. cookie value
2030
+                // of '0' is valid, because 389ds
2031
+                $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
2032
+                $this->search($filter, $base, $attr, $limit, $reOffset, true);
2033
+                if (!$this->hasMoreResults()) {
2034
+                    // when the cookie is reset with != 0 offset, there are no further
2035
+                    // results, so stop.
2036
+                    return false;
2037
+                }
2038
+            }
2039
+            if ($this->lastCookie !== '' && $offset === 0) {
2040
+                //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
2041
+                $this->abandonPagedSearch();
2042
+            }
2043
+            $pagedSearchOK = true === $this->invokeLDAPMethod(
2044
+                    'controlPagedResult', $this->connection->getConnectionResource(), $limit, false
2045
+                );
2046
+            if ($pagedSearchOK) {
2047
+                \OC::$server->getLogger()->debug('Ready for a paged search', ['app' => 'user_ldap']);
2048
+            }
2049
+            /* ++ Fixing RHDS searches with pages with zero results ++
2050 2050
 			 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
2051 2051
 			 * due to pages with zero results.
2052 2052
 			 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
2053 2053
 			 * if we don't have a previous paged search.
2054 2054
 			 */
2055
-		} elseif ($limit === 0 && !empty($this->lastCookie)) {
2056
-			// a search without limit was requested. However, if we do use
2057
-			// Paged Search once, we always must do it. This requires us to
2058
-			// initialize it with the configured page size.
2059
-			$this->abandonPagedSearch();
2060
-			// in case someone set it to 0 … use 500, otherwise no results will
2061
-			// be returned.
2062
-			$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
2063
-			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2064
-				$this->connection->getConnectionResource(),
2065
-				$pageSize, false);
2066
-		}
2067
-
2068
-		return $pagedSearchOK;
2069
-	}
2070
-
2071
-	/**
2072
-	 * Is more than one $attr used for search?
2073
-	 *
2074
-	 * @param string|string[]|null $attr
2075
-	 * @return bool
2076
-	 */
2077
-	private function manyAttributes($attr): bool {
2078
-		if (\is_array($attr)) {
2079
-			return \count($attr) > 1;
2080
-		}
2081
-		return false;
2082
-	}
2055
+        } elseif ($limit === 0 && !empty($this->lastCookie)) {
2056
+            // a search without limit was requested. However, if we do use
2057
+            // Paged Search once, we always must do it. This requires us to
2058
+            // initialize it with the configured page size.
2059
+            $this->abandonPagedSearch();
2060
+            // in case someone set it to 0 … use 500, otherwise no results will
2061
+            // be returned.
2062
+            $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
2063
+            $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2064
+                $this->connection->getConnectionResource(),
2065
+                $pageSize, false);
2066
+        }
2067
+
2068
+        return $pagedSearchOK;
2069
+    }
2070
+
2071
+    /**
2072
+     * Is more than one $attr used for search?
2073
+     *
2074
+     * @param string|string[]|null $attr
2075
+     * @return bool
2076
+     */
2077
+    private function manyAttributes($attr): bool {
2078
+        if (\is_array($attr)) {
2079
+            return \count($attr) > 1;
2080
+        }
2081
+        return false;
2082
+    }
2083 2083
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Connection.php 1 patch
Indentation   +615 added lines, -615 removed lines patch added patch discarded remove patch
@@ -74,619 +74,619 @@
 block discarded – undo
74 74
  * @property int useMemberOfToDetectMembership
75 75
  */
76 76
 class Connection extends LDAPUtility {
77
-	private $ldapConnectionRes = null;
78
-	private $configPrefix;
79
-	private $configID;
80
-	private $configured = false;
81
-	//whether connection should be kept on __destruct
82
-	private $dontDestruct = false;
83
-
84
-	/**
85
-	 * @var bool runtime flag that indicates whether supported primary groups are available
86
-	 */
87
-	public $hasPrimaryGroups = true;
88
-
89
-	/**
90
-	 * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
91
-	 */
92
-	public $hasGidNumber = true;
93
-
94
-	//cache handler
95
-	protected $cache;
96
-
97
-	/** @var Configuration settings handler **/
98
-	protected $configuration;
99
-
100
-	protected $doNotValidate = false;
101
-
102
-	protected $ignoreValidation = false;
103
-
104
-	protected $bindResult = [];
105
-
106
-	/**
107
-	 * Constructor
108
-	 * @param ILDAPWrapper $ldap
109
-	 * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
110
-	 * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
111
-	 */
112
-	public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') {
113
-		parent::__construct($ldap);
114
-		$this->configPrefix = $configPrefix;
115
-		$this->configID = $configID;
116
-		$this->configuration = new Configuration($configPrefix,
117
-												 !is_null($configID));
118
-		$memcache = \OC::$server->getMemCacheFactory();
119
-		if ($memcache->isAvailable()) {
120
-			$this->cache = $memcache->createDistributed();
121
-		}
122
-		$helper = new Helper(\OC::$server->getConfig());
123
-		$this->doNotValidate = !in_array($this->configPrefix,
124
-			$helper->getServerConfigurationPrefixes());
125
-	}
126
-
127
-	public function __destruct() {
128
-		if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
129
-			@$this->ldap->unbind($this->ldapConnectionRes);
130
-			$this->bindResult = [];
131
-		}
132
-	}
133
-
134
-	/**
135
-	 * defines behaviour when the instance is cloned
136
-	 */
137
-	public function __clone() {
138
-		$this->configuration = new Configuration($this->configPrefix,
139
-												 !is_null($this->configID));
140
-		if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
141
-			$this->bindResult = [];
142
-		}
143
-		$this->ldapConnectionRes = null;
144
-		$this->dontDestruct = true;
145
-	}
146
-
147
-	/**
148
-	 * @param string $name
149
-	 * @return bool|mixed
150
-	 */
151
-	public function __get($name) {
152
-		if (!$this->configured) {
153
-			$this->readConfiguration();
154
-		}
155
-
156
-		return $this->configuration->$name;
157
-	}
158
-
159
-	/**
160
-	 * @param string $name
161
-	 * @param mixed $value
162
-	 */
163
-	public function __set($name, $value) {
164
-		$this->doNotValidate = false;
165
-		$before = $this->configuration->$name;
166
-		$this->configuration->$name = $value;
167
-		$after = $this->configuration->$name;
168
-		if ($before !== $after) {
169
-			if ($this->configID !== '' && $this->configID !== null) {
170
-				$this->configuration->saveConfiguration();
171
-			}
172
-			$this->validateConfiguration();
173
-		}
174
-	}
175
-
176
-	/**
177
-	 * @param string $rule
178
-	 * @return array
179
-	 * @throws \RuntimeException
180
-	 */
181
-	public function resolveRule($rule) {
182
-		return $this->configuration->resolveRule($rule);
183
-	}
184
-
185
-	/**
186
-	 * sets whether the result of the configuration validation shall
187
-	 * be ignored when establishing the connection. Used by the Wizard
188
-	 * in early configuration state.
189
-	 * @param bool $state
190
-	 */
191
-	public function setIgnoreValidation($state) {
192
-		$this->ignoreValidation = (bool)$state;
193
-	}
194
-
195
-	/**
196
-	 * initializes the LDAP backend
197
-	 * @param bool $force read the config settings no matter what
198
-	 */
199
-	public function init($force = false) {
200
-		$this->readConfiguration($force);
201
-		$this->establishConnection();
202
-	}
203
-
204
-	/**
205
-	 * Returns the LDAP handler
206
-	 */
207
-	public function getConnectionResource() {
208
-		if (!$this->ldapConnectionRes) {
209
-			$this->init();
210
-		} elseif (!$this->ldap->isResource($this->ldapConnectionRes)) {
211
-			$this->ldapConnectionRes = null;
212
-			$this->establishConnection();
213
-		}
214
-		if (is_null($this->ldapConnectionRes)) {
215
-			\OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, ILogger::ERROR);
216
-			throw new ServerNotAvailableException('Connection to LDAP server could not be established');
217
-		}
218
-		return $this->ldapConnectionRes;
219
-	}
220
-
221
-	/**
222
-	 * resets the connection resource
223
-	 */
224
-	public function resetConnectionResource() {
225
-		if (!is_null($this->ldapConnectionRes)) {
226
-			@$this->ldap->unbind($this->ldapConnectionRes);
227
-			$this->ldapConnectionRes = null;
228
-			$this->bindResult = [];
229
-		}
230
-	}
231
-
232
-	/**
233
-	 * @param string|null $key
234
-	 * @return string
235
-	 */
236
-	private function getCacheKey($key) {
237
-		$prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
238
-		if (is_null($key)) {
239
-			return $prefix;
240
-		}
241
-		return $prefix.hash('sha256', $key);
242
-	}
243
-
244
-	/**
245
-	 * @param string $key
246
-	 * @return mixed|null
247
-	 */
248
-	public function getFromCache($key) {
249
-		if (!$this->configured) {
250
-			$this->readConfiguration();
251
-		}
252
-		if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
253
-			return null;
254
-		}
255
-		$key = $this->getCacheKey($key);
256
-
257
-		return json_decode(base64_decode($this->cache->get($key)), true);
258
-	}
259
-
260
-	/**
261
-	 * @param string $key
262
-	 * @param mixed $value
263
-	 *
264
-	 * @return string
265
-	 */
266
-	public function writeToCache($key, $value) {
267
-		if (!$this->configured) {
268
-			$this->readConfiguration();
269
-		}
270
-		if (is_null($this->cache)
271
-			|| !$this->configuration->ldapCacheTTL
272
-			|| !$this->configuration->ldapConfigurationActive) {
273
-			return null;
274
-		}
275
-		$key   = $this->getCacheKey($key);
276
-		$value = base64_encode(json_encode($value));
277
-		$this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
278
-	}
279
-
280
-	public function clearCache() {
281
-		if (!is_null($this->cache)) {
282
-			$this->cache->clear($this->getCacheKey(null));
283
-		}
284
-	}
285
-
286
-	/**
287
-	 * Caches the general LDAP configuration.
288
-	 * @param bool $force optional. true, if the re-read should be forced. defaults
289
-	 * to false.
290
-	 * @return null
291
-	 */
292
-	private function readConfiguration($force = false) {
293
-		if ((!$this->configured || $force) && !is_null($this->configID)) {
294
-			$this->configuration->readConfiguration();
295
-			$this->configured = $this->validateConfiguration();
296
-		}
297
-	}
298
-
299
-	/**
300
-	 * set LDAP configuration with values delivered by an array, not read from configuration
301
-	 * @param array $config array that holds the config parameters in an associated array
302
-	 * @param array &$setParameters optional; array where the set fields will be given to
303
-	 * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
304
-	 */
305
-	public function setConfiguration($config, &$setParameters = null) {
306
-		if (is_null($setParameters)) {
307
-			$setParameters = [];
308
-		}
309
-		$this->doNotValidate = false;
310
-		$this->configuration->setConfiguration($config, $setParameters);
311
-		if (count($setParameters) > 0) {
312
-			$this->configured = $this->validateConfiguration();
313
-		}
314
-
315
-
316
-		return $this->configured;
317
-	}
318
-
319
-	/**
320
-	 * saves the current Configuration in the database and empties the
321
-	 * cache
322
-	 * @return null
323
-	 */
324
-	public function saveConfiguration() {
325
-		$this->configuration->saveConfiguration();
326
-		$this->clearCache();
327
-	}
328
-
329
-	/**
330
-	 * get the current LDAP configuration
331
-	 * @return array
332
-	 */
333
-	public function getConfiguration() {
334
-		$this->readConfiguration();
335
-		$config = $this->configuration->getConfiguration();
336
-		$cta = $this->configuration->getConfigTranslationArray();
337
-		$result = [];
338
-		foreach ($cta as $dbkey => $configkey) {
339
-			switch ($configkey) {
340
-				case 'homeFolderNamingRule':
341
-					if (strpos($config[$configkey], 'attr:') === 0) {
342
-						$result[$dbkey] = substr($config[$configkey], 5);
343
-					} else {
344
-						$result[$dbkey] = '';
345
-					}
346
-					break;
347
-				case 'ldapBase':
348
-				case 'ldapBaseUsers':
349
-				case 'ldapBaseGroups':
350
-				case 'ldapAttributesForUserSearch':
351
-				case 'ldapAttributesForGroupSearch':
352
-					if (is_array($config[$configkey])) {
353
-						$result[$dbkey] = implode("\n", $config[$configkey]);
354
-						break;
355
-					} //else follows default
356
-					// no break
357
-				default:
358
-					$result[$dbkey] = $config[$configkey];
359
-			}
360
-		}
361
-		return $result;
362
-	}
363
-
364
-	private function doSoftValidation() {
365
-		//if User or Group Base are not set, take over Base DN setting
366
-		foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
367
-			$val = $this->configuration->$keyBase;
368
-			if (empty($val)) {
369
-				$this->configuration->$keyBase = $this->configuration->ldapBase;
370
-			}
371
-		}
372
-
373
-		foreach (['ldapExpertUUIDUserAttr'  => 'ldapUuidUserAttribute',
374
-			'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute']
375
-				as $expertSetting => $effectiveSetting) {
376
-			$uuidOverride = $this->configuration->$expertSetting;
377
-			if (!empty($uuidOverride)) {
378
-				$this->configuration->$effectiveSetting = $uuidOverride;
379
-			} else {
380
-				$uuidAttributes = Access::UUID_ATTRIBUTES;
381
-				array_unshift($uuidAttributes, 'auto');
382
-				if (!in_array($this->configuration->$effectiveSetting,
383
-							$uuidAttributes)
384
-					&& (!is_null($this->configID))) {
385
-					$this->configuration->$effectiveSetting = 'auto';
386
-					$this->configuration->saveConfiguration();
387
-					\OCP\Util::writeLog('user_ldap',
388
-										'Illegal value for the '.
389
-										$effectiveSetting.', '.'reset to '.
390
-										'autodetect.', ILogger::INFO);
391
-				}
392
-			}
393
-		}
394
-
395
-		$backupPort = (int)$this->configuration->ldapBackupPort;
396
-		if ($backupPort <= 0) {
397
-			$this->configuration->backupPort = $this->configuration->ldapPort;
398
-		}
399
-
400
-		//make sure empty search attributes are saved as simple, empty array
401
-		$saKeys = ['ldapAttributesForUserSearch',
402
-			'ldapAttributesForGroupSearch'];
403
-		foreach ($saKeys as $key) {
404
-			$val = $this->configuration->$key;
405
-			if (is_array($val) && count($val) === 1 && empty($val[0])) {
406
-				$this->configuration->$key = [];
407
-			}
408
-		}
409
-
410
-		if ((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
411
-			&& $this->configuration->ldapTLS) {
412
-			$this->configuration->ldapTLS = false;
413
-			\OCP\Util::writeLog(
414
-				'user_ldap',
415
-				'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
416
-				ILogger::INFO
417
-			);
418
-		}
419
-	}
420
-
421
-	/**
422
-	 * @return bool
423
-	 */
424
-	private function doCriticalValidation() {
425
-		$configurationOK = true;
426
-		$errorStr = 'Configuration Error (prefix '.
427
-			(string)$this->configPrefix .'): ';
428
-
429
-		//options that shall not be empty
430
-		$options = ['ldapHost', 'ldapPort', 'ldapUserDisplayName',
431
-			'ldapGroupDisplayName', 'ldapLoginFilter'];
432
-		foreach ($options as $key) {
433
-			$val = $this->configuration->$key;
434
-			if (empty($val)) {
435
-				switch ($key) {
436
-					case 'ldapHost':
437
-						$subj = 'LDAP Host';
438
-						break;
439
-					case 'ldapPort':
440
-						$subj = 'LDAP Port';
441
-						break;
442
-					case 'ldapUserDisplayName':
443
-						$subj = 'LDAP User Display Name';
444
-						break;
445
-					case 'ldapGroupDisplayName':
446
-						$subj = 'LDAP Group Display Name';
447
-						break;
448
-					case 'ldapLoginFilter':
449
-						$subj = 'LDAP Login Filter';
450
-						break;
451
-					default:
452
-						$subj = $key;
453
-						break;
454
-				}
455
-				$configurationOK = false;
456
-				\OCP\Util::writeLog(
457
-					'user_ldap',
458
-					$errorStr.'No '.$subj.' given!',
459
-					ILogger::WARN
460
-				);
461
-			}
462
-		}
463
-
464
-		//combinations
465
-		$agent = $this->configuration->ldapAgentName;
466
-		$pwd = $this->configuration->ldapAgentPassword;
467
-		if (
468
-			($agent === ''  && $pwd !== '')
469
-			|| ($agent !== '' && $pwd === '')
470
-		) {
471
-			\OCP\Util::writeLog(
472
-				'user_ldap',
473
-				$errorStr.'either no password is given for the user ' .
474
-					'agent or a password is given, but not an LDAP agent.',
475
-				ILogger::WARN);
476
-			$configurationOK = false;
477
-		}
478
-
479
-		$base = $this->configuration->ldapBase;
480
-		$baseUsers = $this->configuration->ldapBaseUsers;
481
-		$baseGroups = $this->configuration->ldapBaseGroups;
482
-
483
-		if (empty($base) && empty($baseUsers) && empty($baseGroups)) {
484
-			\OCP\Util::writeLog(
485
-				'user_ldap',
486
-				$errorStr.'Not a single Base DN given.',
487
-				ILogger::WARN
488
-			);
489
-			$configurationOK = false;
490
-		}
491
-
492
-		if (mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
493
-		   === false) {
494
-			\OCP\Util::writeLog(
495
-				'user_ldap',
496
-				$errorStr.'login filter does not contain %uid place holder.',
497
-				ILogger::WARN
498
-			);
499
-			$configurationOK = false;
500
-		}
501
-
502
-		return $configurationOK;
503
-	}
504
-
505
-	/**
506
-	 * Validates the user specified configuration
507
-	 * @return bool true if configuration seems OK, false otherwise
508
-	 */
509
-	private function validateConfiguration() {
510
-		if ($this->doNotValidate) {
511
-			//don't do a validation if it is a new configuration with pure
512
-			//default values. Will be allowed on changes via __set or
513
-			//setConfiguration
514
-			return false;
515
-		}
516
-
517
-		// first step: "soft" checks: settings that are not really
518
-		// necessary, but advisable. If left empty, give an info message
519
-		$this->doSoftValidation();
520
-
521
-		//second step: critical checks. If left empty or filled wrong, mark as
522
-		//not configured and give a warning.
523
-		return $this->doCriticalValidation();
524
-	}
525
-
526
-
527
-	/**
528
-	 * Connects and Binds to LDAP
529
-	 *
530
-	 * @throws ServerNotAvailableException
531
-	 */
532
-	private function establishConnection() {
533
-		if (!$this->configuration->ldapConfigurationActive) {
534
-			return null;
535
-		}
536
-		static $phpLDAPinstalled = true;
537
-		if (!$phpLDAPinstalled) {
538
-			return false;
539
-		}
540
-		if (!$this->ignoreValidation && !$this->configured) {
541
-			\OCP\Util::writeLog(
542
-				'user_ldap',
543
-				'Configuration is invalid, cannot connect',
544
-				ILogger::WARN
545
-			);
546
-			return false;
547
-		}
548
-		if (!$this->ldapConnectionRes) {
549
-			if (!$this->ldap->areLDAPFunctionsAvailable()) {
550
-				$phpLDAPinstalled = false;
551
-				\OCP\Util::writeLog(
552
-					'user_ldap',
553
-					'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
554
-					ILogger::ERROR
555
-				);
556
-
557
-				return false;
558
-			}
559
-			if ($this->configuration->turnOffCertCheck) {
560
-				if (putenv('LDAPTLS_REQCERT=never')) {
561
-					\OCP\Util::writeLog('user_ldap',
562
-						'Turned off SSL certificate validation successfully.',
563
-						ILogger::DEBUG);
564
-				} else {
565
-					\OCP\Util::writeLog(
566
-						'user_ldap',
567
-						'Could not turn off SSL certificate validation.',
568
-						ILogger::WARN
569
-					);
570
-				}
571
-			}
572
-
573
-			$isOverrideMainServer = ($this->configuration->ldapOverrideMainServer
574
-				|| $this->getFromCache('overrideMainServer'));
575
-			$isBackupHost = (trim($this->configuration->ldapBackupHost) !== "");
576
-			$bindStatus = false;
577
-			try {
578
-				if (!$isOverrideMainServer) {
579
-					$this->doConnect($this->configuration->ldapHost,
580
-						$this->configuration->ldapPort);
581
-					return $this->bind();
582
-				}
583
-			} catch (ServerNotAvailableException $e) {
584
-				if (!$isBackupHost) {
585
-					throw $e;
586
-				}
587
-			}
588
-
589
-			//if LDAP server is not reachable, try the Backup (Replica!) Server
590
-			if ($isBackupHost || $isOverrideMainServer) {
591
-				$this->doConnect($this->configuration->ldapBackupHost,
592
-								 $this->configuration->ldapBackupPort);
593
-				$this->bindResult = [];
594
-				$bindStatus = $this->bind();
595
-				$error = $this->ldap->isResource($this->ldapConnectionRes) ?
596
-					$this->ldap->errno($this->ldapConnectionRes) : -1;
597
-				if ($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) {
598
-					//when bind to backup server succeeded and failed to main server,
599
-					//skip contacting him until next cache refresh
600
-					$this->writeToCache('overrideMainServer', true);
601
-				}
602
-			}
603
-
604
-			return $bindStatus;
605
-		}
606
-		return null;
607
-	}
608
-
609
-	/**
610
-	 * @param string $host
611
-	 * @param string $port
612
-	 * @return bool
613
-	 * @throws \OC\ServerNotAvailableException
614
-	 */
615
-	private function doConnect($host, $port) {
616
-		if ($host === '') {
617
-			return false;
618
-		}
619
-
620
-		$this->ldapConnectionRes = $this->ldap->connect($host, $port);
621
-
622
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
623
-			throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
624
-		}
625
-
626
-		if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
627
-			throw new ServerNotAvailableException('Could not disable LDAP referrals.');
628
-		}
629
-
630
-		if ($this->configuration->ldapTLS) {
631
-			if (!$this->ldap->startTls($this->ldapConnectionRes)) {
632
-				throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
633
-			}
634
-		}
635
-
636
-		return true;
637
-	}
638
-
639
-	/**
640
-	 * Binds to LDAP
641
-	 */
642
-	public function bind() {
643
-		if (!$this->configuration->ldapConfigurationActive) {
644
-			return false;
645
-		}
646
-		$cr = $this->ldapConnectionRes;
647
-		if (!$this->ldap->isResource($cr)) {
648
-			$cr = $this->getConnectionResource();
649
-		}
650
-
651
-		if (
652
-			count($this->bindResult) !== 0
653
-			&& $this->bindResult['dn'] === $this->configuration->ldapAgentName
654
-			&& \OC::$server->getHasher()->verify(
655
-				$this->configPrefix . $this->configuration->ldapAgentPassword,
656
-				$this->bindResult['hash']
657
-			)
658
-		) {
659
-			// don't attempt to bind again with the same data as before
660
-			// bind might have been invoked via getConnectionResource(),
661
-			// but we need results specifically for e.g. user login
662
-			return $this->bindResult['result'];
663
-		}
664
-
665
-		$ldapLogin = @$this->ldap->bind($cr,
666
-										$this->configuration->ldapAgentName,
667
-										$this->configuration->ldapAgentPassword);
668
-
669
-		$this->bindResult = [
670
-			'dn' => $this->configuration->ldapAgentName,
671
-			'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword),
672
-			'result' => $ldapLogin,
673
-		];
674
-
675
-		if (!$ldapLogin) {
676
-			$errno = $this->ldap->errno($cr);
677
-
678
-			\OCP\Util::writeLog('user_ldap',
679
-				'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
680
-				ILogger::WARN);
681
-
682
-			// Set to failure mode, if LDAP error code is not LDAP_SUCCESS or LDAP_INVALID_CREDENTIALS
683
-			// or (needed for Apple Open Directory:) LDAP_INSUFFICIENT_ACCESS
684
-			if ($errno !== 0 && $errno !== 49 && $errno !== 50) {
685
-				$this->ldapConnectionRes = null;
686
-			}
687
-
688
-			return false;
689
-		}
690
-		return true;
691
-	}
77
+    private $ldapConnectionRes = null;
78
+    private $configPrefix;
79
+    private $configID;
80
+    private $configured = false;
81
+    //whether connection should be kept on __destruct
82
+    private $dontDestruct = false;
83
+
84
+    /**
85
+     * @var bool runtime flag that indicates whether supported primary groups are available
86
+     */
87
+    public $hasPrimaryGroups = true;
88
+
89
+    /**
90
+     * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
91
+     */
92
+    public $hasGidNumber = true;
93
+
94
+    //cache handler
95
+    protected $cache;
96
+
97
+    /** @var Configuration settings handler **/
98
+    protected $configuration;
99
+
100
+    protected $doNotValidate = false;
101
+
102
+    protected $ignoreValidation = false;
103
+
104
+    protected $bindResult = [];
105
+
106
+    /**
107
+     * Constructor
108
+     * @param ILDAPWrapper $ldap
109
+     * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
110
+     * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
111
+     */
112
+    public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') {
113
+        parent::__construct($ldap);
114
+        $this->configPrefix = $configPrefix;
115
+        $this->configID = $configID;
116
+        $this->configuration = new Configuration($configPrefix,
117
+                                                    !is_null($configID));
118
+        $memcache = \OC::$server->getMemCacheFactory();
119
+        if ($memcache->isAvailable()) {
120
+            $this->cache = $memcache->createDistributed();
121
+        }
122
+        $helper = new Helper(\OC::$server->getConfig());
123
+        $this->doNotValidate = !in_array($this->configPrefix,
124
+            $helper->getServerConfigurationPrefixes());
125
+    }
126
+
127
+    public function __destruct() {
128
+        if (!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
129
+            @$this->ldap->unbind($this->ldapConnectionRes);
130
+            $this->bindResult = [];
131
+        }
132
+    }
133
+
134
+    /**
135
+     * defines behaviour when the instance is cloned
136
+     */
137
+    public function __clone() {
138
+        $this->configuration = new Configuration($this->configPrefix,
139
+                                                    !is_null($this->configID));
140
+        if (count($this->bindResult) !== 0 && $this->bindResult['result'] === true) {
141
+            $this->bindResult = [];
142
+        }
143
+        $this->ldapConnectionRes = null;
144
+        $this->dontDestruct = true;
145
+    }
146
+
147
+    /**
148
+     * @param string $name
149
+     * @return bool|mixed
150
+     */
151
+    public function __get($name) {
152
+        if (!$this->configured) {
153
+            $this->readConfiguration();
154
+        }
155
+
156
+        return $this->configuration->$name;
157
+    }
158
+
159
+    /**
160
+     * @param string $name
161
+     * @param mixed $value
162
+     */
163
+    public function __set($name, $value) {
164
+        $this->doNotValidate = false;
165
+        $before = $this->configuration->$name;
166
+        $this->configuration->$name = $value;
167
+        $after = $this->configuration->$name;
168
+        if ($before !== $after) {
169
+            if ($this->configID !== '' && $this->configID !== null) {
170
+                $this->configuration->saveConfiguration();
171
+            }
172
+            $this->validateConfiguration();
173
+        }
174
+    }
175
+
176
+    /**
177
+     * @param string $rule
178
+     * @return array
179
+     * @throws \RuntimeException
180
+     */
181
+    public function resolveRule($rule) {
182
+        return $this->configuration->resolveRule($rule);
183
+    }
184
+
185
+    /**
186
+     * sets whether the result of the configuration validation shall
187
+     * be ignored when establishing the connection. Used by the Wizard
188
+     * in early configuration state.
189
+     * @param bool $state
190
+     */
191
+    public function setIgnoreValidation($state) {
192
+        $this->ignoreValidation = (bool)$state;
193
+    }
194
+
195
+    /**
196
+     * initializes the LDAP backend
197
+     * @param bool $force read the config settings no matter what
198
+     */
199
+    public function init($force = false) {
200
+        $this->readConfiguration($force);
201
+        $this->establishConnection();
202
+    }
203
+
204
+    /**
205
+     * Returns the LDAP handler
206
+     */
207
+    public function getConnectionResource() {
208
+        if (!$this->ldapConnectionRes) {
209
+            $this->init();
210
+        } elseif (!$this->ldap->isResource($this->ldapConnectionRes)) {
211
+            $this->ldapConnectionRes = null;
212
+            $this->establishConnection();
213
+        }
214
+        if (is_null($this->ldapConnectionRes)) {
215
+            \OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, ILogger::ERROR);
216
+            throw new ServerNotAvailableException('Connection to LDAP server could not be established');
217
+        }
218
+        return $this->ldapConnectionRes;
219
+    }
220
+
221
+    /**
222
+     * resets the connection resource
223
+     */
224
+    public function resetConnectionResource() {
225
+        if (!is_null($this->ldapConnectionRes)) {
226
+            @$this->ldap->unbind($this->ldapConnectionRes);
227
+            $this->ldapConnectionRes = null;
228
+            $this->bindResult = [];
229
+        }
230
+    }
231
+
232
+    /**
233
+     * @param string|null $key
234
+     * @return string
235
+     */
236
+    private function getCacheKey($key) {
237
+        $prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
238
+        if (is_null($key)) {
239
+            return $prefix;
240
+        }
241
+        return $prefix.hash('sha256', $key);
242
+    }
243
+
244
+    /**
245
+     * @param string $key
246
+     * @return mixed|null
247
+     */
248
+    public function getFromCache($key) {
249
+        if (!$this->configured) {
250
+            $this->readConfiguration();
251
+        }
252
+        if (is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
253
+            return null;
254
+        }
255
+        $key = $this->getCacheKey($key);
256
+
257
+        return json_decode(base64_decode($this->cache->get($key)), true);
258
+    }
259
+
260
+    /**
261
+     * @param string $key
262
+     * @param mixed $value
263
+     *
264
+     * @return string
265
+     */
266
+    public function writeToCache($key, $value) {
267
+        if (!$this->configured) {
268
+            $this->readConfiguration();
269
+        }
270
+        if (is_null($this->cache)
271
+            || !$this->configuration->ldapCacheTTL
272
+            || !$this->configuration->ldapConfigurationActive) {
273
+            return null;
274
+        }
275
+        $key   = $this->getCacheKey($key);
276
+        $value = base64_encode(json_encode($value));
277
+        $this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
278
+    }
279
+
280
+    public function clearCache() {
281
+        if (!is_null($this->cache)) {
282
+            $this->cache->clear($this->getCacheKey(null));
283
+        }
284
+    }
285
+
286
+    /**
287
+     * Caches the general LDAP configuration.
288
+     * @param bool $force optional. true, if the re-read should be forced. defaults
289
+     * to false.
290
+     * @return null
291
+     */
292
+    private function readConfiguration($force = false) {
293
+        if ((!$this->configured || $force) && !is_null($this->configID)) {
294
+            $this->configuration->readConfiguration();
295
+            $this->configured = $this->validateConfiguration();
296
+        }
297
+    }
298
+
299
+    /**
300
+     * set LDAP configuration with values delivered by an array, not read from configuration
301
+     * @param array $config array that holds the config parameters in an associated array
302
+     * @param array &$setParameters optional; array where the set fields will be given to
303
+     * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
304
+     */
305
+    public function setConfiguration($config, &$setParameters = null) {
306
+        if (is_null($setParameters)) {
307
+            $setParameters = [];
308
+        }
309
+        $this->doNotValidate = false;
310
+        $this->configuration->setConfiguration($config, $setParameters);
311
+        if (count($setParameters) > 0) {
312
+            $this->configured = $this->validateConfiguration();
313
+        }
314
+
315
+
316
+        return $this->configured;
317
+    }
318
+
319
+    /**
320
+     * saves the current Configuration in the database and empties the
321
+     * cache
322
+     * @return null
323
+     */
324
+    public function saveConfiguration() {
325
+        $this->configuration->saveConfiguration();
326
+        $this->clearCache();
327
+    }
328
+
329
+    /**
330
+     * get the current LDAP configuration
331
+     * @return array
332
+     */
333
+    public function getConfiguration() {
334
+        $this->readConfiguration();
335
+        $config = $this->configuration->getConfiguration();
336
+        $cta = $this->configuration->getConfigTranslationArray();
337
+        $result = [];
338
+        foreach ($cta as $dbkey => $configkey) {
339
+            switch ($configkey) {
340
+                case 'homeFolderNamingRule':
341
+                    if (strpos($config[$configkey], 'attr:') === 0) {
342
+                        $result[$dbkey] = substr($config[$configkey], 5);
343
+                    } else {
344
+                        $result[$dbkey] = '';
345
+                    }
346
+                    break;
347
+                case 'ldapBase':
348
+                case 'ldapBaseUsers':
349
+                case 'ldapBaseGroups':
350
+                case 'ldapAttributesForUserSearch':
351
+                case 'ldapAttributesForGroupSearch':
352
+                    if (is_array($config[$configkey])) {
353
+                        $result[$dbkey] = implode("\n", $config[$configkey]);
354
+                        break;
355
+                    } //else follows default
356
+                    // no break
357
+                default:
358
+                    $result[$dbkey] = $config[$configkey];
359
+            }
360
+        }
361
+        return $result;
362
+    }
363
+
364
+    private function doSoftValidation() {
365
+        //if User or Group Base are not set, take over Base DN setting
366
+        foreach (['ldapBaseUsers', 'ldapBaseGroups'] as $keyBase) {
367
+            $val = $this->configuration->$keyBase;
368
+            if (empty($val)) {
369
+                $this->configuration->$keyBase = $this->configuration->ldapBase;
370
+            }
371
+        }
372
+
373
+        foreach (['ldapExpertUUIDUserAttr'  => 'ldapUuidUserAttribute',
374
+            'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute']
375
+                as $expertSetting => $effectiveSetting) {
376
+            $uuidOverride = $this->configuration->$expertSetting;
377
+            if (!empty($uuidOverride)) {
378
+                $this->configuration->$effectiveSetting = $uuidOverride;
379
+            } else {
380
+                $uuidAttributes = Access::UUID_ATTRIBUTES;
381
+                array_unshift($uuidAttributes, 'auto');
382
+                if (!in_array($this->configuration->$effectiveSetting,
383
+                            $uuidAttributes)
384
+                    && (!is_null($this->configID))) {
385
+                    $this->configuration->$effectiveSetting = 'auto';
386
+                    $this->configuration->saveConfiguration();
387
+                    \OCP\Util::writeLog('user_ldap',
388
+                                        'Illegal value for the '.
389
+                                        $effectiveSetting.', '.'reset to '.
390
+                                        'autodetect.', ILogger::INFO);
391
+                }
392
+            }
393
+        }
394
+
395
+        $backupPort = (int)$this->configuration->ldapBackupPort;
396
+        if ($backupPort <= 0) {
397
+            $this->configuration->backupPort = $this->configuration->ldapPort;
398
+        }
399
+
400
+        //make sure empty search attributes are saved as simple, empty array
401
+        $saKeys = ['ldapAttributesForUserSearch',
402
+            'ldapAttributesForGroupSearch'];
403
+        foreach ($saKeys as $key) {
404
+            $val = $this->configuration->$key;
405
+            if (is_array($val) && count($val) === 1 && empty($val[0])) {
406
+                $this->configuration->$key = [];
407
+            }
408
+        }
409
+
410
+        if ((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
411
+            && $this->configuration->ldapTLS) {
412
+            $this->configuration->ldapTLS = false;
413
+            \OCP\Util::writeLog(
414
+                'user_ldap',
415
+                'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
416
+                ILogger::INFO
417
+            );
418
+        }
419
+    }
420
+
421
+    /**
422
+     * @return bool
423
+     */
424
+    private function doCriticalValidation() {
425
+        $configurationOK = true;
426
+        $errorStr = 'Configuration Error (prefix '.
427
+            (string)$this->configPrefix .'): ';
428
+
429
+        //options that shall not be empty
430
+        $options = ['ldapHost', 'ldapPort', 'ldapUserDisplayName',
431
+            'ldapGroupDisplayName', 'ldapLoginFilter'];
432
+        foreach ($options as $key) {
433
+            $val = $this->configuration->$key;
434
+            if (empty($val)) {
435
+                switch ($key) {
436
+                    case 'ldapHost':
437
+                        $subj = 'LDAP Host';
438
+                        break;
439
+                    case 'ldapPort':
440
+                        $subj = 'LDAP Port';
441
+                        break;
442
+                    case 'ldapUserDisplayName':
443
+                        $subj = 'LDAP User Display Name';
444
+                        break;
445
+                    case 'ldapGroupDisplayName':
446
+                        $subj = 'LDAP Group Display Name';
447
+                        break;
448
+                    case 'ldapLoginFilter':
449
+                        $subj = 'LDAP Login Filter';
450
+                        break;
451
+                    default:
452
+                        $subj = $key;
453
+                        break;
454
+                }
455
+                $configurationOK = false;
456
+                \OCP\Util::writeLog(
457
+                    'user_ldap',
458
+                    $errorStr.'No '.$subj.' given!',
459
+                    ILogger::WARN
460
+                );
461
+            }
462
+        }
463
+
464
+        //combinations
465
+        $agent = $this->configuration->ldapAgentName;
466
+        $pwd = $this->configuration->ldapAgentPassword;
467
+        if (
468
+            ($agent === ''  && $pwd !== '')
469
+            || ($agent !== '' && $pwd === '')
470
+        ) {
471
+            \OCP\Util::writeLog(
472
+                'user_ldap',
473
+                $errorStr.'either no password is given for the user ' .
474
+                    'agent or a password is given, but not an LDAP agent.',
475
+                ILogger::WARN);
476
+            $configurationOK = false;
477
+        }
478
+
479
+        $base = $this->configuration->ldapBase;
480
+        $baseUsers = $this->configuration->ldapBaseUsers;
481
+        $baseGroups = $this->configuration->ldapBaseGroups;
482
+
483
+        if (empty($base) && empty($baseUsers) && empty($baseGroups)) {
484
+            \OCP\Util::writeLog(
485
+                'user_ldap',
486
+                $errorStr.'Not a single Base DN given.',
487
+                ILogger::WARN
488
+            );
489
+            $configurationOK = false;
490
+        }
491
+
492
+        if (mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
493
+            === false) {
494
+            \OCP\Util::writeLog(
495
+                'user_ldap',
496
+                $errorStr.'login filter does not contain %uid place holder.',
497
+                ILogger::WARN
498
+            );
499
+            $configurationOK = false;
500
+        }
501
+
502
+        return $configurationOK;
503
+    }
504
+
505
+    /**
506
+     * Validates the user specified configuration
507
+     * @return bool true if configuration seems OK, false otherwise
508
+     */
509
+    private function validateConfiguration() {
510
+        if ($this->doNotValidate) {
511
+            //don't do a validation if it is a new configuration with pure
512
+            //default values. Will be allowed on changes via __set or
513
+            //setConfiguration
514
+            return false;
515
+        }
516
+
517
+        // first step: "soft" checks: settings that are not really
518
+        // necessary, but advisable. If left empty, give an info message
519
+        $this->doSoftValidation();
520
+
521
+        //second step: critical checks. If left empty or filled wrong, mark as
522
+        //not configured and give a warning.
523
+        return $this->doCriticalValidation();
524
+    }
525
+
526
+
527
+    /**
528
+     * Connects and Binds to LDAP
529
+     *
530
+     * @throws ServerNotAvailableException
531
+     */
532
+    private function establishConnection() {
533
+        if (!$this->configuration->ldapConfigurationActive) {
534
+            return null;
535
+        }
536
+        static $phpLDAPinstalled = true;
537
+        if (!$phpLDAPinstalled) {
538
+            return false;
539
+        }
540
+        if (!$this->ignoreValidation && !$this->configured) {
541
+            \OCP\Util::writeLog(
542
+                'user_ldap',
543
+                'Configuration is invalid, cannot connect',
544
+                ILogger::WARN
545
+            );
546
+            return false;
547
+        }
548
+        if (!$this->ldapConnectionRes) {
549
+            if (!$this->ldap->areLDAPFunctionsAvailable()) {
550
+                $phpLDAPinstalled = false;
551
+                \OCP\Util::writeLog(
552
+                    'user_ldap',
553
+                    'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
554
+                    ILogger::ERROR
555
+                );
556
+
557
+                return false;
558
+            }
559
+            if ($this->configuration->turnOffCertCheck) {
560
+                if (putenv('LDAPTLS_REQCERT=never')) {
561
+                    \OCP\Util::writeLog('user_ldap',
562
+                        'Turned off SSL certificate validation successfully.',
563
+                        ILogger::DEBUG);
564
+                } else {
565
+                    \OCP\Util::writeLog(
566
+                        'user_ldap',
567
+                        'Could not turn off SSL certificate validation.',
568
+                        ILogger::WARN
569
+                    );
570
+                }
571
+            }
572
+
573
+            $isOverrideMainServer = ($this->configuration->ldapOverrideMainServer
574
+                || $this->getFromCache('overrideMainServer'));
575
+            $isBackupHost = (trim($this->configuration->ldapBackupHost) !== "");
576
+            $bindStatus = false;
577
+            try {
578
+                if (!$isOverrideMainServer) {
579
+                    $this->doConnect($this->configuration->ldapHost,
580
+                        $this->configuration->ldapPort);
581
+                    return $this->bind();
582
+                }
583
+            } catch (ServerNotAvailableException $e) {
584
+                if (!$isBackupHost) {
585
+                    throw $e;
586
+                }
587
+            }
588
+
589
+            //if LDAP server is not reachable, try the Backup (Replica!) Server
590
+            if ($isBackupHost || $isOverrideMainServer) {
591
+                $this->doConnect($this->configuration->ldapBackupHost,
592
+                                    $this->configuration->ldapBackupPort);
593
+                $this->bindResult = [];
594
+                $bindStatus = $this->bind();
595
+                $error = $this->ldap->isResource($this->ldapConnectionRes) ?
596
+                    $this->ldap->errno($this->ldapConnectionRes) : -1;
597
+                if ($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) {
598
+                    //when bind to backup server succeeded and failed to main server,
599
+                    //skip contacting him until next cache refresh
600
+                    $this->writeToCache('overrideMainServer', true);
601
+                }
602
+            }
603
+
604
+            return $bindStatus;
605
+        }
606
+        return null;
607
+    }
608
+
609
+    /**
610
+     * @param string $host
611
+     * @param string $port
612
+     * @return bool
613
+     * @throws \OC\ServerNotAvailableException
614
+     */
615
+    private function doConnect($host, $port) {
616
+        if ($host === '') {
617
+            return false;
618
+        }
619
+
620
+        $this->ldapConnectionRes = $this->ldap->connect($host, $port);
621
+
622
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
623
+            throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
624
+        }
625
+
626
+        if (!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
627
+            throw new ServerNotAvailableException('Could not disable LDAP referrals.');
628
+        }
629
+
630
+        if ($this->configuration->ldapTLS) {
631
+            if (!$this->ldap->startTls($this->ldapConnectionRes)) {
632
+                throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
633
+            }
634
+        }
635
+
636
+        return true;
637
+    }
638
+
639
+    /**
640
+     * Binds to LDAP
641
+     */
642
+    public function bind() {
643
+        if (!$this->configuration->ldapConfigurationActive) {
644
+            return false;
645
+        }
646
+        $cr = $this->ldapConnectionRes;
647
+        if (!$this->ldap->isResource($cr)) {
648
+            $cr = $this->getConnectionResource();
649
+        }
650
+
651
+        if (
652
+            count($this->bindResult) !== 0
653
+            && $this->bindResult['dn'] === $this->configuration->ldapAgentName
654
+            && \OC::$server->getHasher()->verify(
655
+                $this->configPrefix . $this->configuration->ldapAgentPassword,
656
+                $this->bindResult['hash']
657
+            )
658
+        ) {
659
+            // don't attempt to bind again with the same data as before
660
+            // bind might have been invoked via getConnectionResource(),
661
+            // but we need results specifically for e.g. user login
662
+            return $this->bindResult['result'];
663
+        }
664
+
665
+        $ldapLogin = @$this->ldap->bind($cr,
666
+                                        $this->configuration->ldapAgentName,
667
+                                        $this->configuration->ldapAgentPassword);
668
+
669
+        $this->bindResult = [
670
+            'dn' => $this->configuration->ldapAgentName,
671
+            'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword),
672
+            'result' => $ldapLogin,
673
+        ];
674
+
675
+        if (!$ldapLogin) {
676
+            $errno = $this->ldap->errno($cr);
677
+
678
+            \OCP\Util::writeLog('user_ldap',
679
+                'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
680
+                ILogger::WARN);
681
+
682
+            // Set to failure mode, if LDAP error code is not LDAP_SUCCESS or LDAP_INVALID_CREDENTIALS
683
+            // or (needed for Apple Open Directory:) LDAP_INSUFFICIENT_ACCESS
684
+            if ($errno !== 0 && $errno !== 49 && $errno !== 50) {
685
+                $this->ldapConnectionRes = null;
686
+            }
687
+
688
+            return false;
689
+        }
690
+        return true;
691
+    }
692 692
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Group_LDAP.php 2 patches
Indentation   +1183 added lines, -1183 removed lines patch added patch discarded remove patch
@@ -53,1187 +53,1187 @@
 block discarded – undo
53 53
 use OCP\ILogger;
54 54
 
55 55
 class Group_LDAP extends BackendUtility implements GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
56
-	protected $enabled = false;
57
-
58
-	/** @var string[] $cachedGroupMembers array of users with gid as key */
59
-	protected $cachedGroupMembers;
60
-	/** @var string[] $cachedGroupsByMember array of groups with uid as key */
61
-	protected $cachedGroupsByMember;
62
-	/** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
63
-	protected $cachedNestedGroups;
64
-	/** @var GroupPluginManager */
65
-	protected $groupPluginManager;
66
-	/** @var ILogger */
67
-	protected $logger;
68
-
69
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
70
-		parent::__construct($access);
71
-		$filter = $this->access->connection->ldapGroupFilter;
72
-		$gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
73
-		if (!empty($filter) && !empty($gAssoc)) {
74
-			$this->enabled = true;
75
-		}
76
-
77
-		$this->cachedGroupMembers = new CappedMemoryCache();
78
-		$this->cachedGroupsByMember = new CappedMemoryCache();
79
-		$this->cachedNestedGroups = new CappedMemoryCache();
80
-		$this->groupPluginManager = $groupPluginManager;
81
-		$this->logger = OC::$server->getLogger();
82
-	}
83
-
84
-	/**
85
-	 * is user in group?
86
-	 *
87
-	 * @param string $uid uid of the user
88
-	 * @param string $gid gid of the group
89
-	 * @return bool
90
-	 * @throws Exception
91
-	 * @throws ServerNotAvailableException
92
-	 */
93
-	public function inGroup($uid, $gid) {
94
-		if (!$this->enabled) {
95
-			return false;
96
-		}
97
-		$cacheKey = 'inGroup' . $uid . ':' . $gid;
98
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
99
-		if (!is_null($inGroup)) {
100
-			return (bool)$inGroup;
101
-		}
102
-
103
-		$userDN = $this->access->username2dn($uid);
104
-
105
-		if (isset($this->cachedGroupMembers[$gid])) {
106
-			return in_array($userDN, $this->cachedGroupMembers[$gid]);
107
-		}
108
-
109
-		$cacheKeyMembers = 'inGroup-members:' . $gid;
110
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
111
-		if (!is_null($members)) {
112
-			$this->cachedGroupMembers[$gid] = $members;
113
-			$isInGroup = in_array($userDN, $members, true);
114
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
115
-			return $isInGroup;
116
-		}
117
-
118
-		$groupDN = $this->access->groupname2dn($gid);
119
-		// just in case
120
-		if (!$groupDN || !$userDN) {
121
-			$this->access->connection->writeToCache($cacheKey, false);
122
-			return false;
123
-		}
124
-
125
-		//check primary group first
126
-		if ($gid === $this->getUserPrimaryGroup($userDN)) {
127
-			$this->access->connection->writeToCache($cacheKey, true);
128
-			return true;
129
-		}
130
-
131
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132
-		$members = $this->_groupMembers($groupDN);
133
-		if (!is_array($members) || count($members) === 0) {
134
-			$this->access->connection->writeToCache($cacheKey, false);
135
-			return false;
136
-		}
137
-
138
-		//extra work if we don't get back user DNs
139
-		if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
140
-			$requestAttributes = $this->access->userManager->getAttributes(true);
141
-			$dns = [];
142
-			$filterParts = [];
143
-			$bytes = 0;
144
-			foreach ($members as $mid) {
145
-				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
146
-				$filterParts[] = $filter;
147
-				$bytes += strlen($filter);
148
-				if ($bytes >= 9000000) {
149
-					// AD has a default input buffer of 10 MB, we do not want
150
-					// to take even the chance to exceed it
151
-					$filter = $this->access->combineFilterWithOr($filterParts);
152
-					$bytes = 0;
153
-					$filterParts = [];
154
-					$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
155
-					$dns = array_merge($dns, $users);
156
-				}
157
-			}
158
-			if (count($filterParts) > 0) {
159
-				$filter = $this->access->combineFilterWithOr($filterParts);
160
-				$users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
161
-				$dns = array_merge($dns, $users);
162
-			}
163
-			$members = $dns;
164
-		}
165
-
166
-		$isInGroup = in_array($userDN, $members);
167
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
168
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
169
-		$this->cachedGroupMembers[$gid] = $members;
170
-
171
-		return $isInGroup;
172
-	}
173
-
174
-	/**
175
-	 * For a group that has user membership defined by an LDAP search url
176
-	 * attribute returns the users that match the search url otherwise returns
177
-	 * an empty array.
178
-	 *
179
-	 * @throws ServerNotAvailableException
180
-	 */
181
-	public function getDynamicGroupMembers(string $dnGroup): array {
182
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
183
-
184
-		if (empty($dynamicGroupMemberURL)) {
185
-			return [];
186
-		}
187
-
188
-		$dynamicMembers = [];
189
-		$memberURLs = $this->access->readAttribute(
190
-			$dnGroup,
191
-			$dynamicGroupMemberURL,
192
-			$this->access->connection->ldapGroupFilter
193
-		);
194
-		if ($memberURLs !== false) {
195
-			// this group has the 'memberURL' attribute so this is a dynamic group
196
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
197
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
198
-			$pos = strpos($memberURLs[0], '(');
199
-			if ($pos !== false) {
200
-				$memberUrlFilter = substr($memberURLs[0], $pos);
201
-				$foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
202
-				$dynamicMembers = [];
203
-				foreach ($foundMembers as $value) {
204
-					$dynamicMembers[$value['dn'][0]] = 1;
205
-				}
206
-			} else {
207
-				$this->logger->debug('No search filter found on member url of group {dn}',
208
-					[
209
-						'app' => 'user_ldap',
210
-						'dn' => $dnGroup,
211
-					]
212
-				);
213
-			}
214
-		}
215
-		return $dynamicMembers;
216
-	}
217
-
218
-	/**
219
-	 * @throws ServerNotAvailableException
220
-	 */
221
-	private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
222
-		if ($seen === null) {
223
-			$seen = [];
224
-		}
225
-		$allMembers = [];
226
-		if (array_key_exists($dnGroup, $seen)) {
227
-			return [];
228
-		}
229
-		// used extensively in cron job, caching makes sense for nested groups
230
-		$cacheKey = '_groupMembers' . $dnGroup;
231
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
232
-		if ($groupMembers !== null) {
233
-			return $groupMembers;
234
-		}
235
-		$seen[$dnGroup] = 1;
236
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
237
-		if (is_array($members)) {
238
-			$fetcher = function ($memberDN, &$seen) {
239
-				return $this->_groupMembers($memberDN, $seen);
240
-			};
241
-			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
242
-		}
243
-
244
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
245
-
246
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
247
-		return $allMembers;
248
-	}
249
-
250
-	/**
251
-	 * @throws ServerNotAvailableException
252
-	 */
253
-	private function _getGroupDNsFromMemberOf(string $dn): array {
254
-		$groups = $this->access->readAttribute($dn, 'memberOf');
255
-		if (!is_array($groups)) {
256
-			return [];
257
-		}
258
-
259
-		$fetcher = function ($groupDN) {
260
-			if (isset($this->cachedNestedGroups[$groupDN])) {
261
-				$nestedGroups = $this->cachedNestedGroups[$groupDN];
262
-			} else {
263
-				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
264
-				if (!is_array($nestedGroups)) {
265
-					$nestedGroups = [];
266
-				}
267
-				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
268
-			}
269
-			return $nestedGroups;
270
-		};
271
-
272
-		$groups = $this->walkNestedGroups($dn, $fetcher, $groups);
273
-		return $this->filterValidGroups($groups);
274
-	}
275
-
276
-	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
277
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
278
-		// depending on the input, we either have a list of DNs or a list of LDAP records
279
-		// also, the output expects either DNs or records. Testing the first element should suffice.
280
-		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
281
-
282
-		if ($nesting !== 1) {
283
-			if ($recordMode) {
284
-				// the keys are numeric, but should hold the DN
285
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
286
-					if ($record['dn'][0] != $dn) {
287
-						$transformed[$record['dn'][0]] = $record;
288
-					}
289
-					return $transformed;
290
-				}, []);
291
-			}
292
-			return $list;
293
-		}
294
-
295
-		$seen = [];
296
-		while ($record = array_pop($list)) {
297
-			$recordDN = $recordMode ? $record['dn'][0] : $record;
298
-			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
299
-				// Prevent loops
300
-				continue;
301
-			}
302
-			$fetched = $fetcher($record, $seen);
303
-			$list = array_merge($list, $fetched);
304
-			$seen[$recordDN] = $record;
305
-		}
306
-
307
-		return $recordMode ? $seen : array_keys($seen);
308
-	}
309
-
310
-	/**
311
-	 * translates a gidNumber into an ownCloud internal name
312
-	 *
313
-	 * @return string|bool
314
-	 * @throws Exception
315
-	 * @throws ServerNotAvailableException
316
-	 */
317
-	public function gidNumber2Name(string $gid, string $dn) {
318
-		$cacheKey = 'gidNumberToName' . $gid;
319
-		$groupName = $this->access->connection->getFromCache($cacheKey);
320
-		if (!is_null($groupName) && isset($groupName)) {
321
-			return $groupName;
322
-		}
323
-
324
-		//we need to get the DN from LDAP
325
-		$filter = $this->access->combineFilterWithAnd([
326
-			$this->access->connection->ldapGroupFilter,
327
-			'objectClass=posixGroup',
328
-			$this->access->connection->ldapGidNumber . '=' . $gid
329
-		]);
330
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
331
-	}
332
-
333
-	/**
334
-	 * @throws ServerNotAvailableException
335
-	 * @throws Exception
336
-	 */
337
-	private function getNameOfGroup(string $filter, string $cacheKey) {
338
-		$result = $this->access->searchGroups($filter, ['dn'], 1);
339
-		if (empty($result)) {
340
-			return null;
341
-		}
342
-		$dn = $result[0]['dn'][0];
343
-
344
-		//and now the group name
345
-		//NOTE once we have separate Nextcloud group IDs and group names we can
346
-		//directly read the display name attribute instead of the DN
347
-		$name = $this->access->dn2groupname($dn);
348
-
349
-		$this->access->connection->writeToCache($cacheKey, $name);
350
-
351
-		return $name;
352
-	}
353
-
354
-	/**
355
-	 * returns the entry's gidNumber
356
-	 *
357
-	 * @return string|bool
358
-	 * @throws ServerNotAvailableException
359
-	 */
360
-	private function getEntryGidNumber(string $dn, string $attribute) {
361
-		$value = $this->access->readAttribute($dn, $attribute);
362
-		if (is_array($value) && !empty($value)) {
363
-			return $value[0];
364
-		}
365
-		return false;
366
-	}
367
-
368
-	/**
369
-	 * @return string|bool
370
-	 * @throws ServerNotAvailableException
371
-	 */
372
-	public function getGroupGidNumber(string $dn) {
373
-		return $this->getEntryGidNumber($dn, 'gidNumber');
374
-	}
375
-
376
-	/**
377
-	 * returns the user's gidNumber
378
-	 *
379
-	 * @return string|bool
380
-	 * @throws ServerNotAvailableException
381
-	 */
382
-	public function getUserGidNumber(string $dn) {
383
-		$gidNumber = false;
384
-		if ($this->access->connection->hasGidNumber) {
385
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
386
-			if ($gidNumber === false) {
387
-				$this->access->connection->hasGidNumber = false;
388
-			}
389
-		}
390
-		return $gidNumber;
391
-	}
392
-
393
-	/**
394
-	 * @throws ServerNotAvailableException
395
-	 * @throws Exception
396
-	 */
397
-	private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
398
-		$groupID = $this->getGroupGidNumber($groupDN);
399
-		if ($groupID === false) {
400
-			throw new Exception('Not a valid group');
401
-		}
402
-
403
-		$filterParts = [];
404
-		$filterParts[] = $this->access->getFilterForUserCount();
405
-		if ($search !== '') {
406
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
407
-		}
408
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
409
-
410
-		return $this->access->combineFilterWithAnd($filterParts);
411
-	}
412
-
413
-	/**
414
-	 * returns a list of users that have the given group as gid number
415
-	 *
416
-	 * @throws ServerNotAvailableException
417
-	 */
418
-	public function getUsersInGidNumber(
419
-		string $groupDN,
420
-		string $search = '',
421
-		?int $limit = -1,
422
-		?int $offset = 0
423
-	): array {
424
-		try {
425
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
426
-			$users = $this->access->fetchListOfUsers(
427
-				$filter,
428
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
429
-				$limit,
430
-				$offset
431
-			);
432
-			return $this->access->nextcloudUserNames($users);
433
-		} catch (ServerNotAvailableException $e) {
434
-			throw $e;
435
-		} catch (Exception $e) {
436
-			return [];
437
-		}
438
-	}
439
-
440
-	/**
441
-	 * @throws ServerNotAvailableException
442
-	 * @return bool
443
-	 */
444
-	public function getUserGroupByGid(string $dn) {
445
-		$groupID = $this->getUserGidNumber($dn);
446
-		if ($groupID !== false) {
447
-			$groupName = $this->gidNumber2Name($groupID, $dn);
448
-			if ($groupName !== false) {
449
-				return $groupName;
450
-			}
451
-		}
452
-
453
-		return false;
454
-	}
455
-
456
-	/**
457
-	 * translates a primary group ID into an Nextcloud internal name
458
-	 *
459
-	 * @return string|bool
460
-	 * @throws Exception
461
-	 * @throws ServerNotAvailableException
462
-	 */
463
-	public function primaryGroupID2Name(string $gid, string $dn) {
464
-		$cacheKey = 'primaryGroupIDtoName';
465
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
466
-		if (!is_null($groupNames) && isset($groupNames[$gid])) {
467
-			return $groupNames[$gid];
468
-		}
469
-
470
-		$domainObjectSid = $this->access->getSID($dn);
471
-		if ($domainObjectSid === false) {
472
-			return false;
473
-		}
474
-
475
-		//we need to get the DN from LDAP
476
-		$filter = $this->access->combineFilterWithAnd([
477
-			$this->access->connection->ldapGroupFilter,
478
-			'objectsid=' . $domainObjectSid . '-' . $gid
479
-		]);
480
-		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
481
-	}
482
-
483
-	/**
484
-	 * returns the entry's primary group ID
485
-	 *
486
-	 * @return string|bool
487
-	 * @throws ServerNotAvailableException
488
-	 */
489
-	private function getEntryGroupID(string $dn, string $attribute) {
490
-		$value = $this->access->readAttribute($dn, $attribute);
491
-		if (is_array($value) && !empty($value)) {
492
-			return $value[0];
493
-		}
494
-		return false;
495
-	}
496
-
497
-	/**
498
-	 * @return string|bool
499
-	 * @throws ServerNotAvailableException
500
-	 */
501
-	public function getGroupPrimaryGroupID(string $dn) {
502
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
503
-	}
504
-
505
-	/**
506
-	 * @return string|bool
507
-	 * @throws ServerNotAvailableException
508
-	 */
509
-	public function getUserPrimaryGroupIDs(string $dn) {
510
-		$primaryGroupID = false;
511
-		if ($this->access->connection->hasPrimaryGroups) {
512
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
513
-			if ($primaryGroupID === false) {
514
-				$this->access->connection->hasPrimaryGroups = false;
515
-			}
516
-		}
517
-		return $primaryGroupID;
518
-	}
519
-
520
-	/**
521
-	 * @throws Exception
522
-	 * @throws ServerNotAvailableException
523
-	 */
524
-	private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
525
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
526
-		if ($groupID === false) {
527
-			throw new Exception('Not a valid group');
528
-		}
529
-
530
-		$filterParts = [];
531
-		$filterParts[] = $this->access->getFilterForUserCount();
532
-		if ($search !== '') {
533
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
-		}
535
-		$filterParts[] = 'primaryGroupID=' . $groupID;
536
-
537
-		return $this->access->combineFilterWithAnd($filterParts);
538
-	}
539
-
540
-	/**
541
-	 * @throws ServerNotAvailableException
542
-	 */
543
-	public function getUsersInPrimaryGroup(
544
-		string $groupDN,
545
-		string $search = '',
546
-		?int $limit = -1,
547
-		?int $offset = 0
548
-	): array {
549
-		try {
550
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
551
-			$users = $this->access->fetchListOfUsers(
552
-				$filter,
553
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
554
-				$limit,
555
-				$offset
556
-			);
557
-			return $this->access->nextcloudUserNames($users);
558
-		} catch (ServerNotAvailableException $e) {
559
-			throw $e;
560
-		} catch (Exception $e) {
561
-			return [];
562
-		}
563
-	}
564
-
565
-	/**
566
-	 * @throws ServerNotAvailableException
567
-	 */
568
-	public function countUsersInPrimaryGroup(
569
-		string $groupDN,
570
-		string $search = '',
571
-		int $limit = -1,
572
-		int $offset = 0
573
-	): int {
574
-		try {
575
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
577
-			return (int)$users;
578
-		} catch (ServerNotAvailableException $e) {
579
-			throw $e;
580
-		} catch (Exception $e) {
581
-			return 0;
582
-		}
583
-	}
584
-
585
-	/**
586
-	 * @return string|bool
587
-	 * @throws ServerNotAvailableException
588
-	 */
589
-	public function getUserPrimaryGroup(string $dn) {
590
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
591
-		if ($groupID !== false) {
592
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
593
-			if ($groupName !== false) {
594
-				return $groupName;
595
-			}
596
-		}
597
-
598
-		return false;
599
-	}
600
-
601
-	/**
602
-	 * This function fetches all groups a user belongs to. It does not check
603
-	 * if the user exists at all.
604
-	 *
605
-	 * This function includes groups based on dynamic group membership.
606
-	 *
607
-	 * @param string $uid Name of the user
608
-	 * @return array with group names
609
-	 * @throws Exception
610
-	 * @throws ServerNotAvailableException
611
-	 */
612
-	public function getUserGroups($uid) {
613
-		if (!$this->enabled) {
614
-			return [];
615
-		}
616
-		$cacheKey = 'getUserGroups' . $uid;
617
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
618
-		if (!is_null($userGroups)) {
619
-			return $userGroups;
620
-		}
621
-		$userDN = $this->access->username2dn($uid);
622
-		if (!$userDN) {
623
-			$this->access->connection->writeToCache($cacheKey, []);
624
-			return [];
625
-		}
626
-
627
-		$groups = [];
628
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
629
-		$gidGroupName = $this->getUserGroupByGid($userDN);
630
-
631
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
632
-
633
-		if (!empty($dynamicGroupMemberURL)) {
634
-			// look through dynamic groups to add them to the result array if needed
635
-			$groupsToMatch = $this->access->fetchListOfGroups(
636
-				$this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
637
-			foreach ($groupsToMatch as $dynamicGroup) {
638
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
639
-					continue;
640
-				}
641
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
642
-				if ($pos !== false) {
643
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
644
-					// apply filter via ldap search to see if this user is in this
645
-					// dynamic group
646
-					$userMatch = $this->access->readAttribute(
647
-						$userDN,
648
-						$this->access->connection->ldapUserDisplayName,
649
-						$memberUrlFilter
650
-					);
651
-					if ($userMatch !== false) {
652
-						// match found so this user is in this group
653
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
654
-						if (is_string($groupName)) {
655
-							// be sure to never return false if the dn could not be
656
-							// resolved to a name, for whatever reason.
657
-							$groups[] = $groupName;
658
-						}
659
-					}
660
-				} else {
661
-					$this->logger->debug('No search filter found on member url of group {dn}',
662
-						[
663
-							'app' => 'user_ldap',
664
-							'dn' => $dynamicGroup,
665
-						]
666
-					);
667
-				}
668
-			}
669
-		}
670
-
671
-		// if possible, read out membership via memberOf. It's far faster than
672
-		// performing a search, which still is a fallback later.
673
-		// memberof doesn't support memberuid, so skip it here.
674
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
675
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
676
-			&& strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
677
-		) {
678
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
679
-			if (is_array($groupDNs)) {
680
-				foreach ($groupDNs as $dn) {
681
-					$groupName = $this->access->dn2groupname($dn);
682
-					if (is_string($groupName)) {
683
-						// be sure to never return false if the dn could not be
684
-						// resolved to a name, for whatever reason.
685
-						$groups[] = $groupName;
686
-					}
687
-				}
688
-			}
689
-
690
-			if ($primaryGroup !== false) {
691
-				$groups[] = $primaryGroup;
692
-			}
693
-			if ($gidGroupName !== false) {
694
-				$groups[] = $gidGroupName;
695
-			}
696
-			$this->access->connection->writeToCache($cacheKey, $groups);
697
-			return $groups;
698
-		}
699
-
700
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
701
-		if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
702
-			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
703
-		) {
704
-			$uid = $userDN;
705
-		} elseif (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
706
-			$result = $this->access->readAttribute($userDN, 'uid');
707
-			if ($result === false) {
708
-				$this->logger->debug('No uid attribute found for DN {dn} on {host}',
709
-					[
710
-						'app' => 'user_ldap',
711
-						'dn' => $userDN,
712
-						'host' => $this->access->connection->ldapHost,
713
-					]
714
-				);
715
-				$uid = false;
716
-			} else {
717
-				$uid = $result[0];
718
-			}
719
-		} else {
720
-			// just in case
721
-			$uid = $userDN;
722
-		}
723
-
724
-		if ($uid !== false) {
725
-			if (isset($this->cachedGroupsByMember[$uid])) {
726
-				$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
727
-			} else {
728
-				$groupsByMember = array_values($this->getGroupsByMember($uid));
729
-				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
730
-				$this->cachedGroupsByMember[$uid] = $groupsByMember;
731
-				$groups = array_merge($groups, $groupsByMember);
732
-			}
733
-		}
734
-
735
-		if ($primaryGroup !== false) {
736
-			$groups[] = $primaryGroup;
737
-		}
738
-		if ($gidGroupName !== false) {
739
-			$groups[] = $gidGroupName;
740
-		}
741
-
742
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
743
-		$this->access->connection->writeToCache($cacheKey, $groups);
744
-
745
-		return $groups;
746
-	}
747
-
748
-	/**
749
-	 * @throws ServerNotAvailableException
750
-	 */
751
-	private function getGroupsByMember(string $dn, array &$seen = null): array {
752
-		if ($seen === null) {
753
-			$seen = [];
754
-		}
755
-		if (array_key_exists($dn, $seen)) {
756
-			// avoid loops
757
-			return [];
758
-		}
759
-		$allGroups = [];
760
-		$seen[$dn] = true;
761
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
762
-		$groups = $this->access->fetchListOfGroups($filter,
763
-			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
764
-		if (is_array($groups)) {
765
-			$fetcher = function ($dn, &$seen) {
766
-				if (is_array($dn) && isset($dn['dn'][0])) {
767
-					$dn = $dn['dn'][0];
768
-				}
769
-				return $this->getGroupsByMember($dn, $seen);
770
-			};
771
-			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
772
-		}
773
-		$visibleGroups = $this->filterValidGroups($allGroups);
774
-		return array_intersect_key($allGroups, $visibleGroups);
775
-	}
776
-
777
-	/**
778
-	 * get a list of all users in a group
779
-	 *
780
-	 * @param string $gid
781
-	 * @param string $search
782
-	 * @param int $limit
783
-	 * @param int $offset
784
-	 * @return array with user ids
785
-	 * @throws Exception
786
-	 * @throws ServerNotAvailableException
787
-	 */
788
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
789
-		if (!$this->enabled) {
790
-			return [];
791
-		}
792
-		if (!$this->groupExists($gid)) {
793
-			return [];
794
-		}
795
-		$search = $this->access->escapeFilterPart($search, true);
796
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
797
-		// check for cache of the exact query
798
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
799
-		if (!is_null($groupUsers)) {
800
-			return $groupUsers;
801
-		}
802
-
803
-		if ($limit === -1) {
804
-			$limit = null;
805
-		}
806
-		// check for cache of the query without limit and offset
807
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
808
-		if (!is_null($groupUsers)) {
809
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
810
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
811
-			return $groupUsers;
812
-		}
813
-
814
-		$groupDN = $this->access->groupname2dn($gid);
815
-		if (!$groupDN) {
816
-			// group couldn't be found, return empty resultset
817
-			$this->access->connection->writeToCache($cacheKey, []);
818
-			return [];
819
-		}
820
-
821
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
822
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
823
-		$members = $this->_groupMembers($groupDN);
824
-		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
825
-			//in case users could not be retrieved, return empty result set
826
-			$this->access->connection->writeToCache($cacheKey, []);
827
-			return [];
828
-		}
829
-
830
-		$groupUsers = [];
831
-		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
832
-		$attrs = $this->access->userManager->getAttributes(true);
833
-		foreach ($members as $member) {
834
-			if ($isMemberUid) {
835
-				//we got uids, need to get their DNs to 'translate' them to user names
836
-				$filter = $this->access->combineFilterWithAnd([
837
-					str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
838
-					$this->access->combineFilterWithAnd([
839
-						$this->access->getFilterPartForUserSearch($search),
840
-						$this->access->connection->ldapUserFilter
841
-					])
842
-				]);
843
-				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
844
-				if (count($ldap_users) < 1) {
845
-					continue;
846
-				}
847
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
848
-			} else {
849
-				//we got DNs, check if we need to filter by search or we can give back all of them
850
-				$uid = $this->access->dn2username($member);
851
-				if (!$uid) {
852
-					continue;
853
-				}
854
-
855
-				$cacheKey = 'userExistsOnLDAP' . $uid;
856
-				$userExists = $this->access->connection->getFromCache($cacheKey);
857
-				if ($userExists === false) {
858
-					continue;
859
-				}
860
-				if ($userExists === null || $search !== '') {
861
-					if (!$this->access->readAttribute($member,
862
-						$this->access->connection->ldapUserDisplayName,
863
-						$this->access->combineFilterWithAnd([
864
-							$this->access->getFilterPartForUserSearch($search),
865
-							$this->access->connection->ldapUserFilter
866
-						]))) {
867
-						if ($search === '') {
868
-							$this->access->connection->writeToCache($cacheKey, false);
869
-						}
870
-						continue;
871
-					}
872
-					$this->access->connection->writeToCache($cacheKey, true);
873
-				}
874
-				$groupUsers[] = $uid;
875
-			}
876
-		}
877
-
878
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
879
-		natsort($groupUsers);
880
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
881
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
882
-
883
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
884
-
885
-		return $groupUsers;
886
-	}
887
-
888
-	/**
889
-	 * returns the number of users in a group, who match the search term
890
-	 *
891
-	 * @param string $gid the internal group name
892
-	 * @param string $search optional, a search string
893
-	 * @return int|bool
894
-	 * @throws Exception
895
-	 * @throws ServerNotAvailableException
896
-	 */
897
-	public function countUsersInGroup($gid, $search = '') {
898
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
899
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
900
-		}
901
-
902
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
903
-		if (!$this->enabled || !$this->groupExists($gid)) {
904
-			return false;
905
-		}
906
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
907
-		if (!is_null($groupUsers)) {
908
-			return $groupUsers;
909
-		}
910
-
911
-		$groupDN = $this->access->groupname2dn($gid);
912
-		if (!$groupDN) {
913
-			// group couldn't be found, return empty result set
914
-			$this->access->connection->writeToCache($cacheKey, false);
915
-			return false;
916
-		}
917
-
918
-		$members = $this->_groupMembers($groupDN);
919
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
920
-		if (!$members && $primaryUserCount === 0) {
921
-			//in case users could not be retrieved, return empty result set
922
-			$this->access->connection->writeToCache($cacheKey, false);
923
-			return false;
924
-		}
925
-
926
-		if ($search === '') {
927
-			$groupUsers = count($members) + $primaryUserCount;
928
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
929
-			return $groupUsers;
930
-		}
931
-		$search = $this->access->escapeFilterPart($search, true);
932
-		$isMemberUid =
933
-			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
934
-				=== 'memberuid');
935
-
936
-		//we need to apply the search filter
937
-		//alternatives that need to be checked:
938
-		//a) get all users by search filter and array_intersect them
939
-		//b) a, but only when less than 1k 10k ?k users like it is
940
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
941
-		//   and let it count.
942
-		//For now this is not important, because the only use of this method
943
-		//does not supply a search string
944
-		$groupUsers = [];
945
-		foreach ($members as $member) {
946
-			if ($isMemberUid) {
947
-				//we got uids, need to get their DNs to 'translate' them to user names
948
-				$filter = $this->access->combineFilterWithAnd([
949
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
950
-					$this->access->getFilterPartForUserSearch($search)
951
-				]);
952
-				$ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
953
-				if (count($ldap_users) < 1) {
954
-					continue;
955
-				}
956
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
957
-			} else {
958
-				//we need to apply the search filter now
959
-				if (!$this->access->readAttribute($member,
960
-					$this->access->connection->ldapUserDisplayName,
961
-					$this->access->getFilterPartForUserSearch($search))) {
962
-					continue;
963
-				}
964
-				// dn2username will also check if the users belong to the allowed base
965
-				if ($ncGroupId = $this->access->dn2username($member)) {
966
-					$groupUsers[] = $ncGroupId;
967
-				}
968
-			}
969
-		}
970
-
971
-		//and get users that have the group as primary
972
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
973
-
974
-		return count($groupUsers) + $primaryUsers;
975
-	}
976
-
977
-	/**
978
-	 * get a list of all groups using a paged search
979
-	 *
980
-	 * @param string $search
981
-	 * @param int $limit
982
-	 * @param int $offset
983
-	 * @return array with group names
984
-	 *
985
-	 * Returns a list with all groups
986
-	 * Uses a paged search if available to override a
987
-	 * server side search limit.
988
-	 * (active directory has a limit of 1000 by default)
989
-	 * @throws Exception
990
-	 */
991
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
992
-		if (!$this->enabled) {
993
-			return [];
994
-		}
995
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
996
-
997
-		//Check cache before driving unnecessary searches
998
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
-		if (!is_null($ldap_groups)) {
1000
-			return $ldap_groups;
1001
-		}
1002
-
1003
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004
-		// error. With a limit of 0, we get 0 results. So we pass null.
1005
-		if ($limit <= 0) {
1006
-			$limit = null;
1007
-		}
1008
-		$filter = $this->access->combineFilterWithAnd([
1009
-			$this->access->connection->ldapGroupFilter,
1010
-			$this->access->getFilterPartForGroupSearch($search)
1011
-		]);
1012
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
1013
-			[$this->access->connection->ldapGroupDisplayName, 'dn'],
1014
-			$limit,
1015
-			$offset);
1016
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1017
-
1018
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1019
-		return $ldap_groups;
1020
-	}
1021
-
1022
-	/**
1023
-	 * check if a group exists
1024
-	 *
1025
-	 * @param string $gid
1026
-	 * @return bool
1027
-	 * @throws ServerNotAvailableException
1028
-	 */
1029
-	public function groupExists($gid) {
1030
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1031
-		if (!is_null($groupExists)) {
1032
-			return (bool)$groupExists;
1033
-		}
1034
-
1035
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1036
-		//only, requires more checking.
1037
-		$dn = $this->access->groupname2dn($gid);
1038
-		if (!$dn) {
1039
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1040
-			return false;
1041
-		}
1042
-
1043
-		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1044
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1045
-			return false;
1046
-		}
1047
-
1048
-		//if group really still exists, we will be able to read its objectClass
1049
-		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1050
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1051
-			return false;
1052
-		}
1053
-
1054
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1055
-		return true;
1056
-	}
1057
-
1058
-	/**
1059
-	 * @throws ServerNotAvailableException
1060
-	 * @throws Exception
1061
-	 */
1062
-	protected function filterValidGroups(array $listOfGroups): array {
1063
-		$validGroupDNs = [];
1064
-		foreach ($listOfGroups as $key => $item) {
1065
-			$dn = is_string($item) ? $item : $item['dn'][0];
1066
-			$gid = $this->access->dn2groupname($dn);
1067
-			if (!$gid) {
1068
-				continue;
1069
-			}
1070
-			if ($this->groupExists($gid)) {
1071
-				$validGroupDNs[$key] = $item;
1072
-			}
1073
-		}
1074
-		return $validGroupDNs;
1075
-	}
1076
-
1077
-	/**
1078
-	 * Check if backend implements actions
1079
-	 *
1080
-	 * @param int $actions bitwise-or'ed actions
1081
-	 * @return boolean
1082
-	 *
1083
-	 * Returns the supported actions as int to be
1084
-	 * compared with GroupInterface::CREATE_GROUP etc.
1085
-	 */
1086
-	public function implementsActions($actions) {
1087
-		return (bool)((GroupInterface::COUNT_USERS |
1088
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1089
-	}
1090
-
1091
-	/**
1092
-	 * Return access for LDAP interaction.
1093
-	 *
1094
-	 * @return Access instance of Access for LDAP interaction
1095
-	 */
1096
-	public function getLDAPAccess($gid) {
1097
-		return $this->access;
1098
-	}
1099
-
1100
-	/**
1101
-	 * create a group
1102
-	 *
1103
-	 * @param string $gid
1104
-	 * @return bool
1105
-	 * @throws Exception
1106
-	 * @throws ServerNotAvailableException
1107
-	 */
1108
-	public function createGroup($gid) {
1109
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1110
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1111
-				//updates group mapping
1112
-				$uuid = $this->access->getUUID($dn, false);
1113
-				if (is_string($uuid)) {
1114
-					$this->access->mapAndAnnounceIfApplicable(
1115
-						$this->access->getGroupMapper(),
1116
-						$dn,
1117
-						$gid,
1118
-						$uuid,
1119
-						false
1120
-					);
1121
-					$this->access->cacheGroupExists($gid);
1122
-				}
1123
-			}
1124
-			return $dn != null;
1125
-		}
1126
-		throw new Exception('Could not create group in LDAP backend.');
1127
-	}
1128
-
1129
-	/**
1130
-	 * delete a group
1131
-	 *
1132
-	 * @param string $gid gid of the group to delete
1133
-	 * @return bool
1134
-	 * @throws Exception
1135
-	 */
1136
-	public function deleteGroup($gid) {
1137
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1138
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1139
-				#delete group in nextcloud internal db
1140
-				$this->access->getGroupMapper()->unmap($gid);
1141
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1142
-			}
1143
-			return $ret;
1144
-		}
1145
-		throw new Exception('Could not delete group in LDAP backend.');
1146
-	}
1147
-
1148
-	/**
1149
-	 * Add a user to a group
1150
-	 *
1151
-	 * @param string $uid Name of the user to add to group
1152
-	 * @param string $gid Name of the group in which add the user
1153
-	 * @return bool
1154
-	 * @throws Exception
1155
-	 */
1156
-	public function addToGroup($uid, $gid) {
1157
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1158
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1159
-				$this->access->connection->clearCache();
1160
-				unset($this->cachedGroupMembers[$gid]);
1161
-			}
1162
-			return $ret;
1163
-		}
1164
-		throw new Exception('Could not add user to group in LDAP backend.');
1165
-	}
1166
-
1167
-	/**
1168
-	 * Removes a user from a group
1169
-	 *
1170
-	 * @param string $uid Name of the user to remove from group
1171
-	 * @param string $gid Name of the group from which remove the user
1172
-	 * @return bool
1173
-	 * @throws Exception
1174
-	 */
1175
-	public function removeFromGroup($uid, $gid) {
1176
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1177
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1178
-				$this->access->connection->clearCache();
1179
-				unset($this->cachedGroupMembers[$gid]);
1180
-			}
1181
-			return $ret;
1182
-		}
1183
-		throw new Exception('Could not remove user from group in LDAP backend.');
1184
-	}
1185
-
1186
-	/**
1187
-	 * Gets group details
1188
-	 *
1189
-	 * @param string $gid Name of the group
1190
-	 * @return array|false
1191
-	 * @throws Exception
1192
-	 */
1193
-	public function getGroupDetails($gid) {
1194
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1195
-			return $this->groupPluginManager->getGroupDetails($gid);
1196
-		}
1197
-		throw new Exception('Could not get group details in LDAP backend.');
1198
-	}
1199
-
1200
-	/**
1201
-	 * Return LDAP connection resource from a cloned connection.
1202
-	 * The cloned connection needs to be closed manually.
1203
-	 * of the current access.
1204
-	 *
1205
-	 * @param string $gid
1206
-	 * @return resource of the LDAP connection
1207
-	 * @throws ServerNotAvailableException
1208
-	 */
1209
-	public function getNewLDAPConnection($gid) {
1210
-		$connection = clone $this->access->getConnection();
1211
-		return $connection->getConnectionResource();
1212
-	}
1213
-
1214
-	/**
1215
-	 * @throws ServerNotAvailableException
1216
-	 */
1217
-	public function getDisplayName(string $gid): string {
1218
-		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1219
-			return $this->groupPluginManager->getDisplayName($gid);
1220
-		}
1221
-
1222
-		$cacheKey = 'group_getDisplayName' . $gid;
1223
-		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1224
-			return $displayName;
1225
-		}
1226
-
1227
-		$displayName = $this->access->readAttribute(
1228
-			$this->access->groupname2dn($gid),
1229
-			$this->access->connection->ldapGroupDisplayName);
1230
-
1231
-		if ($displayName && (count($displayName) > 0)) {
1232
-			$displayName = $displayName[0];
1233
-			$this->access->connection->writeToCache($cacheKey, $displayName);
1234
-			return $displayName;
1235
-		}
1236
-
1237
-		return '';
1238
-	}
56
+    protected $enabled = false;
57
+
58
+    /** @var string[] $cachedGroupMembers array of users with gid as key */
59
+    protected $cachedGroupMembers;
60
+    /** @var string[] $cachedGroupsByMember array of groups with uid as key */
61
+    protected $cachedGroupsByMember;
62
+    /** @var string[] $cachedNestedGroups array of groups with gid (DN) as key */
63
+    protected $cachedNestedGroups;
64
+    /** @var GroupPluginManager */
65
+    protected $groupPluginManager;
66
+    /** @var ILogger */
67
+    protected $logger;
68
+
69
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
70
+        parent::__construct($access);
71
+        $filter = $this->access->connection->ldapGroupFilter;
72
+        $gAssoc = $this->access->connection->ldapGroupMemberAssocAttr;
73
+        if (!empty($filter) && !empty($gAssoc)) {
74
+            $this->enabled = true;
75
+        }
76
+
77
+        $this->cachedGroupMembers = new CappedMemoryCache();
78
+        $this->cachedGroupsByMember = new CappedMemoryCache();
79
+        $this->cachedNestedGroups = new CappedMemoryCache();
80
+        $this->groupPluginManager = $groupPluginManager;
81
+        $this->logger = OC::$server->getLogger();
82
+    }
83
+
84
+    /**
85
+     * is user in group?
86
+     *
87
+     * @param string $uid uid of the user
88
+     * @param string $gid gid of the group
89
+     * @return bool
90
+     * @throws Exception
91
+     * @throws ServerNotAvailableException
92
+     */
93
+    public function inGroup($uid, $gid) {
94
+        if (!$this->enabled) {
95
+            return false;
96
+        }
97
+        $cacheKey = 'inGroup' . $uid . ':' . $gid;
98
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
99
+        if (!is_null($inGroup)) {
100
+            return (bool)$inGroup;
101
+        }
102
+
103
+        $userDN = $this->access->username2dn($uid);
104
+
105
+        if (isset($this->cachedGroupMembers[$gid])) {
106
+            return in_array($userDN, $this->cachedGroupMembers[$gid]);
107
+        }
108
+
109
+        $cacheKeyMembers = 'inGroup-members:' . $gid;
110
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
111
+        if (!is_null($members)) {
112
+            $this->cachedGroupMembers[$gid] = $members;
113
+            $isInGroup = in_array($userDN, $members, true);
114
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
115
+            return $isInGroup;
116
+        }
117
+
118
+        $groupDN = $this->access->groupname2dn($gid);
119
+        // just in case
120
+        if (!$groupDN || !$userDN) {
121
+            $this->access->connection->writeToCache($cacheKey, false);
122
+            return false;
123
+        }
124
+
125
+        //check primary group first
126
+        if ($gid === $this->getUserPrimaryGroup($userDN)) {
127
+            $this->access->connection->writeToCache($cacheKey, true);
128
+            return true;
129
+        }
130
+
131
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132
+        $members = $this->_groupMembers($groupDN);
133
+        if (!is_array($members) || count($members) === 0) {
134
+            $this->access->connection->writeToCache($cacheKey, false);
135
+            return false;
136
+        }
137
+
138
+        //extra work if we don't get back user DNs
139
+        if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
140
+            $requestAttributes = $this->access->userManager->getAttributes(true);
141
+            $dns = [];
142
+            $filterParts = [];
143
+            $bytes = 0;
144
+            foreach ($members as $mid) {
145
+                $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
146
+                $filterParts[] = $filter;
147
+                $bytes += strlen($filter);
148
+                if ($bytes >= 9000000) {
149
+                    // AD has a default input buffer of 10 MB, we do not want
150
+                    // to take even the chance to exceed it
151
+                    $filter = $this->access->combineFilterWithOr($filterParts);
152
+                    $bytes = 0;
153
+                    $filterParts = [];
154
+                    $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
155
+                    $dns = array_merge($dns, $users);
156
+                }
157
+            }
158
+            if (count($filterParts) > 0) {
159
+                $filter = $this->access->combineFilterWithOr($filterParts);
160
+                $users = $this->access->fetchListOfUsers($filter, $requestAttributes, count($filterParts));
161
+                $dns = array_merge($dns, $users);
162
+            }
163
+            $members = $dns;
164
+        }
165
+
166
+        $isInGroup = in_array($userDN, $members);
167
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
168
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
169
+        $this->cachedGroupMembers[$gid] = $members;
170
+
171
+        return $isInGroup;
172
+    }
173
+
174
+    /**
175
+     * For a group that has user membership defined by an LDAP search url
176
+     * attribute returns the users that match the search url otherwise returns
177
+     * an empty array.
178
+     *
179
+     * @throws ServerNotAvailableException
180
+     */
181
+    public function getDynamicGroupMembers(string $dnGroup): array {
182
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
183
+
184
+        if (empty($dynamicGroupMemberURL)) {
185
+            return [];
186
+        }
187
+
188
+        $dynamicMembers = [];
189
+        $memberURLs = $this->access->readAttribute(
190
+            $dnGroup,
191
+            $dynamicGroupMemberURL,
192
+            $this->access->connection->ldapGroupFilter
193
+        );
194
+        if ($memberURLs !== false) {
195
+            // this group has the 'memberURL' attribute so this is a dynamic group
196
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
197
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
198
+            $pos = strpos($memberURLs[0], '(');
199
+            if ($pos !== false) {
200
+                $memberUrlFilter = substr($memberURLs[0], $pos);
201
+                $foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
202
+                $dynamicMembers = [];
203
+                foreach ($foundMembers as $value) {
204
+                    $dynamicMembers[$value['dn'][0]] = 1;
205
+                }
206
+            } else {
207
+                $this->logger->debug('No search filter found on member url of group {dn}',
208
+                    [
209
+                        'app' => 'user_ldap',
210
+                        'dn' => $dnGroup,
211
+                    ]
212
+                );
213
+            }
214
+        }
215
+        return $dynamicMembers;
216
+    }
217
+
218
+    /**
219
+     * @throws ServerNotAvailableException
220
+     */
221
+    private function _groupMembers(string $dnGroup, ?array &$seen = null): array {
222
+        if ($seen === null) {
223
+            $seen = [];
224
+        }
225
+        $allMembers = [];
226
+        if (array_key_exists($dnGroup, $seen)) {
227
+            return [];
228
+        }
229
+        // used extensively in cron job, caching makes sense for nested groups
230
+        $cacheKey = '_groupMembers' . $dnGroup;
231
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
232
+        if ($groupMembers !== null) {
233
+            return $groupMembers;
234
+        }
235
+        $seen[$dnGroup] = 1;
236
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
237
+        if (is_array($members)) {
238
+            $fetcher = function ($memberDN, &$seen) {
239
+                return $this->_groupMembers($memberDN, $seen);
240
+            };
241
+            $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
242
+        }
243
+
244
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
245
+
246
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
247
+        return $allMembers;
248
+    }
249
+
250
+    /**
251
+     * @throws ServerNotAvailableException
252
+     */
253
+    private function _getGroupDNsFromMemberOf(string $dn): array {
254
+        $groups = $this->access->readAttribute($dn, 'memberOf');
255
+        if (!is_array($groups)) {
256
+            return [];
257
+        }
258
+
259
+        $fetcher = function ($groupDN) {
260
+            if (isset($this->cachedNestedGroups[$groupDN])) {
261
+                $nestedGroups = $this->cachedNestedGroups[$groupDN];
262
+            } else {
263
+                $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
264
+                if (!is_array($nestedGroups)) {
265
+                    $nestedGroups = [];
266
+                }
267
+                $this->cachedNestedGroups[$groupDN] = $nestedGroups;
268
+            }
269
+            return $nestedGroups;
270
+        };
271
+
272
+        $groups = $this->walkNestedGroups($dn, $fetcher, $groups);
273
+        return $this->filterValidGroups($groups);
274
+    }
275
+
276
+    private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
277
+        $nesting = (int)$this->access->connection->ldapNestedGroups;
278
+        // depending on the input, we either have a list of DNs or a list of LDAP records
279
+        // also, the output expects either DNs or records. Testing the first element should suffice.
280
+        $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
281
+
282
+        if ($nesting !== 1) {
283
+            if ($recordMode) {
284
+                // the keys are numeric, but should hold the DN
285
+                return array_reduce($list, function ($transformed, $record) use ($dn) {
286
+                    if ($record['dn'][0] != $dn) {
287
+                        $transformed[$record['dn'][0]] = $record;
288
+                    }
289
+                    return $transformed;
290
+                }, []);
291
+            }
292
+            return $list;
293
+        }
294
+
295
+        $seen = [];
296
+        while ($record = array_pop($list)) {
297
+            $recordDN = $recordMode ? $record['dn'][0] : $record;
298
+            if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
299
+                // Prevent loops
300
+                continue;
301
+            }
302
+            $fetched = $fetcher($record, $seen);
303
+            $list = array_merge($list, $fetched);
304
+            $seen[$recordDN] = $record;
305
+        }
306
+
307
+        return $recordMode ? $seen : array_keys($seen);
308
+    }
309
+
310
+    /**
311
+     * translates a gidNumber into an ownCloud internal name
312
+     *
313
+     * @return string|bool
314
+     * @throws Exception
315
+     * @throws ServerNotAvailableException
316
+     */
317
+    public function gidNumber2Name(string $gid, string $dn) {
318
+        $cacheKey = 'gidNumberToName' . $gid;
319
+        $groupName = $this->access->connection->getFromCache($cacheKey);
320
+        if (!is_null($groupName) && isset($groupName)) {
321
+            return $groupName;
322
+        }
323
+
324
+        //we need to get the DN from LDAP
325
+        $filter = $this->access->combineFilterWithAnd([
326
+            $this->access->connection->ldapGroupFilter,
327
+            'objectClass=posixGroup',
328
+            $this->access->connection->ldapGidNumber . '=' . $gid
329
+        ]);
330
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
331
+    }
332
+
333
+    /**
334
+     * @throws ServerNotAvailableException
335
+     * @throws Exception
336
+     */
337
+    private function getNameOfGroup(string $filter, string $cacheKey) {
338
+        $result = $this->access->searchGroups($filter, ['dn'], 1);
339
+        if (empty($result)) {
340
+            return null;
341
+        }
342
+        $dn = $result[0]['dn'][0];
343
+
344
+        //and now the group name
345
+        //NOTE once we have separate Nextcloud group IDs and group names we can
346
+        //directly read the display name attribute instead of the DN
347
+        $name = $this->access->dn2groupname($dn);
348
+
349
+        $this->access->connection->writeToCache($cacheKey, $name);
350
+
351
+        return $name;
352
+    }
353
+
354
+    /**
355
+     * returns the entry's gidNumber
356
+     *
357
+     * @return string|bool
358
+     * @throws ServerNotAvailableException
359
+     */
360
+    private function getEntryGidNumber(string $dn, string $attribute) {
361
+        $value = $this->access->readAttribute($dn, $attribute);
362
+        if (is_array($value) && !empty($value)) {
363
+            return $value[0];
364
+        }
365
+        return false;
366
+    }
367
+
368
+    /**
369
+     * @return string|bool
370
+     * @throws ServerNotAvailableException
371
+     */
372
+    public function getGroupGidNumber(string $dn) {
373
+        return $this->getEntryGidNumber($dn, 'gidNumber');
374
+    }
375
+
376
+    /**
377
+     * returns the user's gidNumber
378
+     *
379
+     * @return string|bool
380
+     * @throws ServerNotAvailableException
381
+     */
382
+    public function getUserGidNumber(string $dn) {
383
+        $gidNumber = false;
384
+        if ($this->access->connection->hasGidNumber) {
385
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
386
+            if ($gidNumber === false) {
387
+                $this->access->connection->hasGidNumber = false;
388
+            }
389
+        }
390
+        return $gidNumber;
391
+    }
392
+
393
+    /**
394
+     * @throws ServerNotAvailableException
395
+     * @throws Exception
396
+     */
397
+    private function prepareFilterForUsersHasGidNumber(string $groupDN, string $search = ''): string {
398
+        $groupID = $this->getGroupGidNumber($groupDN);
399
+        if ($groupID === false) {
400
+            throw new Exception('Not a valid group');
401
+        }
402
+
403
+        $filterParts = [];
404
+        $filterParts[] = $this->access->getFilterForUserCount();
405
+        if ($search !== '') {
406
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
407
+        }
408
+        $filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
409
+
410
+        return $this->access->combineFilterWithAnd($filterParts);
411
+    }
412
+
413
+    /**
414
+     * returns a list of users that have the given group as gid number
415
+     *
416
+     * @throws ServerNotAvailableException
417
+     */
418
+    public function getUsersInGidNumber(
419
+        string $groupDN,
420
+        string $search = '',
421
+        ?int $limit = -1,
422
+        ?int $offset = 0
423
+    ): array {
424
+        try {
425
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
426
+            $users = $this->access->fetchListOfUsers(
427
+                $filter,
428
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
429
+                $limit,
430
+                $offset
431
+            );
432
+            return $this->access->nextcloudUserNames($users);
433
+        } catch (ServerNotAvailableException $e) {
434
+            throw $e;
435
+        } catch (Exception $e) {
436
+            return [];
437
+        }
438
+    }
439
+
440
+    /**
441
+     * @throws ServerNotAvailableException
442
+     * @return bool
443
+     */
444
+    public function getUserGroupByGid(string $dn) {
445
+        $groupID = $this->getUserGidNumber($dn);
446
+        if ($groupID !== false) {
447
+            $groupName = $this->gidNumber2Name($groupID, $dn);
448
+            if ($groupName !== false) {
449
+                return $groupName;
450
+            }
451
+        }
452
+
453
+        return false;
454
+    }
455
+
456
+    /**
457
+     * translates a primary group ID into an Nextcloud internal name
458
+     *
459
+     * @return string|bool
460
+     * @throws Exception
461
+     * @throws ServerNotAvailableException
462
+     */
463
+    public function primaryGroupID2Name(string $gid, string $dn) {
464
+        $cacheKey = 'primaryGroupIDtoName';
465
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
466
+        if (!is_null($groupNames) && isset($groupNames[$gid])) {
467
+            return $groupNames[$gid];
468
+        }
469
+
470
+        $domainObjectSid = $this->access->getSID($dn);
471
+        if ($domainObjectSid === false) {
472
+            return false;
473
+        }
474
+
475
+        //we need to get the DN from LDAP
476
+        $filter = $this->access->combineFilterWithAnd([
477
+            $this->access->connection->ldapGroupFilter,
478
+            'objectsid=' . $domainObjectSid . '-' . $gid
479
+        ]);
480
+        return $this->getNameOfGroup($filter, $cacheKey) ?? false;
481
+    }
482
+
483
+    /**
484
+     * returns the entry's primary group ID
485
+     *
486
+     * @return string|bool
487
+     * @throws ServerNotAvailableException
488
+     */
489
+    private function getEntryGroupID(string $dn, string $attribute) {
490
+        $value = $this->access->readAttribute($dn, $attribute);
491
+        if (is_array($value) && !empty($value)) {
492
+            return $value[0];
493
+        }
494
+        return false;
495
+    }
496
+
497
+    /**
498
+     * @return string|bool
499
+     * @throws ServerNotAvailableException
500
+     */
501
+    public function getGroupPrimaryGroupID(string $dn) {
502
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
503
+    }
504
+
505
+    /**
506
+     * @return string|bool
507
+     * @throws ServerNotAvailableException
508
+     */
509
+    public function getUserPrimaryGroupIDs(string $dn) {
510
+        $primaryGroupID = false;
511
+        if ($this->access->connection->hasPrimaryGroups) {
512
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
513
+            if ($primaryGroupID === false) {
514
+                $this->access->connection->hasPrimaryGroups = false;
515
+            }
516
+        }
517
+        return $primaryGroupID;
518
+    }
519
+
520
+    /**
521
+     * @throws Exception
522
+     * @throws ServerNotAvailableException
523
+     */
524
+    private function prepareFilterForUsersInPrimaryGroup(string $groupDN, string $search = ''): string {
525
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
526
+        if ($groupID === false) {
527
+            throw new Exception('Not a valid group');
528
+        }
529
+
530
+        $filterParts = [];
531
+        $filterParts[] = $this->access->getFilterForUserCount();
532
+        if ($search !== '') {
533
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
+        }
535
+        $filterParts[] = 'primaryGroupID=' . $groupID;
536
+
537
+        return $this->access->combineFilterWithAnd($filterParts);
538
+    }
539
+
540
+    /**
541
+     * @throws ServerNotAvailableException
542
+     */
543
+    public function getUsersInPrimaryGroup(
544
+        string $groupDN,
545
+        string $search = '',
546
+        ?int $limit = -1,
547
+        ?int $offset = 0
548
+    ): array {
549
+        try {
550
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
551
+            $users = $this->access->fetchListOfUsers(
552
+                $filter,
553
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
554
+                $limit,
555
+                $offset
556
+            );
557
+            return $this->access->nextcloudUserNames($users);
558
+        } catch (ServerNotAvailableException $e) {
559
+            throw $e;
560
+        } catch (Exception $e) {
561
+            return [];
562
+        }
563
+    }
564
+
565
+    /**
566
+     * @throws ServerNotAvailableException
567
+     */
568
+    public function countUsersInPrimaryGroup(
569
+        string $groupDN,
570
+        string $search = '',
571
+        int $limit = -1,
572
+        int $offset = 0
573
+    ): int {
574
+        try {
575
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
577
+            return (int)$users;
578
+        } catch (ServerNotAvailableException $e) {
579
+            throw $e;
580
+        } catch (Exception $e) {
581
+            return 0;
582
+        }
583
+    }
584
+
585
+    /**
586
+     * @return string|bool
587
+     * @throws ServerNotAvailableException
588
+     */
589
+    public function getUserPrimaryGroup(string $dn) {
590
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
591
+        if ($groupID !== false) {
592
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
593
+            if ($groupName !== false) {
594
+                return $groupName;
595
+            }
596
+        }
597
+
598
+        return false;
599
+    }
600
+
601
+    /**
602
+     * This function fetches all groups a user belongs to. It does not check
603
+     * if the user exists at all.
604
+     *
605
+     * This function includes groups based on dynamic group membership.
606
+     *
607
+     * @param string $uid Name of the user
608
+     * @return array with group names
609
+     * @throws Exception
610
+     * @throws ServerNotAvailableException
611
+     */
612
+    public function getUserGroups($uid) {
613
+        if (!$this->enabled) {
614
+            return [];
615
+        }
616
+        $cacheKey = 'getUserGroups' . $uid;
617
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
618
+        if (!is_null($userGroups)) {
619
+            return $userGroups;
620
+        }
621
+        $userDN = $this->access->username2dn($uid);
622
+        if (!$userDN) {
623
+            $this->access->connection->writeToCache($cacheKey, []);
624
+            return [];
625
+        }
626
+
627
+        $groups = [];
628
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
629
+        $gidGroupName = $this->getUserGroupByGid($userDN);
630
+
631
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
632
+
633
+        if (!empty($dynamicGroupMemberURL)) {
634
+            // look through dynamic groups to add them to the result array if needed
635
+            $groupsToMatch = $this->access->fetchListOfGroups(
636
+                $this->access->connection->ldapGroupFilter, ['dn', $dynamicGroupMemberURL]);
637
+            foreach ($groupsToMatch as $dynamicGroup) {
638
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
639
+                    continue;
640
+                }
641
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
642
+                if ($pos !== false) {
643
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
644
+                    // apply filter via ldap search to see if this user is in this
645
+                    // dynamic group
646
+                    $userMatch = $this->access->readAttribute(
647
+                        $userDN,
648
+                        $this->access->connection->ldapUserDisplayName,
649
+                        $memberUrlFilter
650
+                    );
651
+                    if ($userMatch !== false) {
652
+                        // match found so this user is in this group
653
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
654
+                        if (is_string($groupName)) {
655
+                            // be sure to never return false if the dn could not be
656
+                            // resolved to a name, for whatever reason.
657
+                            $groups[] = $groupName;
658
+                        }
659
+                    }
660
+                } else {
661
+                    $this->logger->debug('No search filter found on member url of group {dn}',
662
+                        [
663
+                            'app' => 'user_ldap',
664
+                            'dn' => $dynamicGroup,
665
+                        ]
666
+                    );
667
+                }
668
+            }
669
+        }
670
+
671
+        // if possible, read out membership via memberOf. It's far faster than
672
+        // performing a search, which still is a fallback later.
673
+        // memberof doesn't support memberuid, so skip it here.
674
+        if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
675
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
676
+            && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
677
+        ) {
678
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
679
+            if (is_array($groupDNs)) {
680
+                foreach ($groupDNs as $dn) {
681
+                    $groupName = $this->access->dn2groupname($dn);
682
+                    if (is_string($groupName)) {
683
+                        // be sure to never return false if the dn could not be
684
+                        // resolved to a name, for whatever reason.
685
+                        $groups[] = $groupName;
686
+                    }
687
+                }
688
+            }
689
+
690
+            if ($primaryGroup !== false) {
691
+                $groups[] = $primaryGroup;
692
+            }
693
+            if ($gidGroupName !== false) {
694
+                $groups[] = $gidGroupName;
695
+            }
696
+            $this->access->connection->writeToCache($cacheKey, $groups);
697
+            return $groups;
698
+        }
699
+
700
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
701
+        if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
702
+            || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
703
+        ) {
704
+            $uid = $userDN;
705
+        } elseif (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
706
+            $result = $this->access->readAttribute($userDN, 'uid');
707
+            if ($result === false) {
708
+                $this->logger->debug('No uid attribute found for DN {dn} on {host}',
709
+                    [
710
+                        'app' => 'user_ldap',
711
+                        'dn' => $userDN,
712
+                        'host' => $this->access->connection->ldapHost,
713
+                    ]
714
+                );
715
+                $uid = false;
716
+            } else {
717
+                $uid = $result[0];
718
+            }
719
+        } else {
720
+            // just in case
721
+            $uid = $userDN;
722
+        }
723
+
724
+        if ($uid !== false) {
725
+            if (isset($this->cachedGroupsByMember[$uid])) {
726
+                $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
727
+            } else {
728
+                $groupsByMember = array_values($this->getGroupsByMember($uid));
729
+                $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
730
+                $this->cachedGroupsByMember[$uid] = $groupsByMember;
731
+                $groups = array_merge($groups, $groupsByMember);
732
+            }
733
+        }
734
+
735
+        if ($primaryGroup !== false) {
736
+            $groups[] = $primaryGroup;
737
+        }
738
+        if ($gidGroupName !== false) {
739
+            $groups[] = $gidGroupName;
740
+        }
741
+
742
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
743
+        $this->access->connection->writeToCache($cacheKey, $groups);
744
+
745
+        return $groups;
746
+    }
747
+
748
+    /**
749
+     * @throws ServerNotAvailableException
750
+     */
751
+    private function getGroupsByMember(string $dn, array &$seen = null): array {
752
+        if ($seen === null) {
753
+            $seen = [];
754
+        }
755
+        if (array_key_exists($dn, $seen)) {
756
+            // avoid loops
757
+            return [];
758
+        }
759
+        $allGroups = [];
760
+        $seen[$dn] = true;
761
+        $filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
762
+        $groups = $this->access->fetchListOfGroups($filter,
763
+            [strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
764
+        if (is_array($groups)) {
765
+            $fetcher = function ($dn, &$seen) {
766
+                if (is_array($dn) && isset($dn['dn'][0])) {
767
+                    $dn = $dn['dn'][0];
768
+                }
769
+                return $this->getGroupsByMember($dn, $seen);
770
+            };
771
+            $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
772
+        }
773
+        $visibleGroups = $this->filterValidGroups($allGroups);
774
+        return array_intersect_key($allGroups, $visibleGroups);
775
+    }
776
+
777
+    /**
778
+     * get a list of all users in a group
779
+     *
780
+     * @param string $gid
781
+     * @param string $search
782
+     * @param int $limit
783
+     * @param int $offset
784
+     * @return array with user ids
785
+     * @throws Exception
786
+     * @throws ServerNotAvailableException
787
+     */
788
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
789
+        if (!$this->enabled) {
790
+            return [];
791
+        }
792
+        if (!$this->groupExists($gid)) {
793
+            return [];
794
+        }
795
+        $search = $this->access->escapeFilterPart($search, true);
796
+        $cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
797
+        // check for cache of the exact query
798
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
799
+        if (!is_null($groupUsers)) {
800
+            return $groupUsers;
801
+        }
802
+
803
+        if ($limit === -1) {
804
+            $limit = null;
805
+        }
806
+        // check for cache of the query without limit and offset
807
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
808
+        if (!is_null($groupUsers)) {
809
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
810
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
811
+            return $groupUsers;
812
+        }
813
+
814
+        $groupDN = $this->access->groupname2dn($gid);
815
+        if (!$groupDN) {
816
+            // group couldn't be found, return empty resultset
817
+            $this->access->connection->writeToCache($cacheKey, []);
818
+            return [];
819
+        }
820
+
821
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
822
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
823
+        $members = $this->_groupMembers($groupDN);
824
+        if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
825
+            //in case users could not be retrieved, return empty result set
826
+            $this->access->connection->writeToCache($cacheKey, []);
827
+            return [];
828
+        }
829
+
830
+        $groupUsers = [];
831
+        $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
832
+        $attrs = $this->access->userManager->getAttributes(true);
833
+        foreach ($members as $member) {
834
+            if ($isMemberUid) {
835
+                //we got uids, need to get their DNs to 'translate' them to user names
836
+                $filter = $this->access->combineFilterWithAnd([
837
+                    str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
838
+                    $this->access->combineFilterWithAnd([
839
+                        $this->access->getFilterPartForUserSearch($search),
840
+                        $this->access->connection->ldapUserFilter
841
+                    ])
842
+                ]);
843
+                $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
844
+                if (count($ldap_users) < 1) {
845
+                    continue;
846
+                }
847
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
848
+            } else {
849
+                //we got DNs, check if we need to filter by search or we can give back all of them
850
+                $uid = $this->access->dn2username($member);
851
+                if (!$uid) {
852
+                    continue;
853
+                }
854
+
855
+                $cacheKey = 'userExistsOnLDAP' . $uid;
856
+                $userExists = $this->access->connection->getFromCache($cacheKey);
857
+                if ($userExists === false) {
858
+                    continue;
859
+                }
860
+                if ($userExists === null || $search !== '') {
861
+                    if (!$this->access->readAttribute($member,
862
+                        $this->access->connection->ldapUserDisplayName,
863
+                        $this->access->combineFilterWithAnd([
864
+                            $this->access->getFilterPartForUserSearch($search),
865
+                            $this->access->connection->ldapUserFilter
866
+                        ]))) {
867
+                        if ($search === '') {
868
+                            $this->access->connection->writeToCache($cacheKey, false);
869
+                        }
870
+                        continue;
871
+                    }
872
+                    $this->access->connection->writeToCache($cacheKey, true);
873
+                }
874
+                $groupUsers[] = $uid;
875
+            }
876
+        }
877
+
878
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
879
+        natsort($groupUsers);
880
+        $this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
881
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
882
+
883
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
884
+
885
+        return $groupUsers;
886
+    }
887
+
888
+    /**
889
+     * returns the number of users in a group, who match the search term
890
+     *
891
+     * @param string $gid the internal group name
892
+     * @param string $search optional, a search string
893
+     * @return int|bool
894
+     * @throws Exception
895
+     * @throws ServerNotAvailableException
896
+     */
897
+    public function countUsersInGroup($gid, $search = '') {
898
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
899
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
900
+        }
901
+
902
+        $cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
903
+        if (!$this->enabled || !$this->groupExists($gid)) {
904
+            return false;
905
+        }
906
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
907
+        if (!is_null($groupUsers)) {
908
+            return $groupUsers;
909
+        }
910
+
911
+        $groupDN = $this->access->groupname2dn($gid);
912
+        if (!$groupDN) {
913
+            // group couldn't be found, return empty result set
914
+            $this->access->connection->writeToCache($cacheKey, false);
915
+            return false;
916
+        }
917
+
918
+        $members = $this->_groupMembers($groupDN);
919
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
920
+        if (!$members && $primaryUserCount === 0) {
921
+            //in case users could not be retrieved, return empty result set
922
+            $this->access->connection->writeToCache($cacheKey, false);
923
+            return false;
924
+        }
925
+
926
+        if ($search === '') {
927
+            $groupUsers = count($members) + $primaryUserCount;
928
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
929
+            return $groupUsers;
930
+        }
931
+        $search = $this->access->escapeFilterPart($search, true);
932
+        $isMemberUid =
933
+            (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
934
+                === 'memberuid');
935
+
936
+        //we need to apply the search filter
937
+        //alternatives that need to be checked:
938
+        //a) get all users by search filter and array_intersect them
939
+        //b) a, but only when less than 1k 10k ?k users like it is
940
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
941
+        //   and let it count.
942
+        //For now this is not important, because the only use of this method
943
+        //does not supply a search string
944
+        $groupUsers = [];
945
+        foreach ($members as $member) {
946
+            if ($isMemberUid) {
947
+                //we got uids, need to get their DNs to 'translate' them to user names
948
+                $filter = $this->access->combineFilterWithAnd([
949
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
950
+                    $this->access->getFilterPartForUserSearch($search)
951
+                ]);
952
+                $ldap_users = $this->access->fetchListOfUsers($filter, ['dn'], 1);
953
+                if (count($ldap_users) < 1) {
954
+                    continue;
955
+                }
956
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
957
+            } else {
958
+                //we need to apply the search filter now
959
+                if (!$this->access->readAttribute($member,
960
+                    $this->access->connection->ldapUserDisplayName,
961
+                    $this->access->getFilterPartForUserSearch($search))) {
962
+                    continue;
963
+                }
964
+                // dn2username will also check if the users belong to the allowed base
965
+                if ($ncGroupId = $this->access->dn2username($member)) {
966
+                    $groupUsers[] = $ncGroupId;
967
+                }
968
+            }
969
+        }
970
+
971
+        //and get users that have the group as primary
972
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
973
+
974
+        return count($groupUsers) + $primaryUsers;
975
+    }
976
+
977
+    /**
978
+     * get a list of all groups using a paged search
979
+     *
980
+     * @param string $search
981
+     * @param int $limit
982
+     * @param int $offset
983
+     * @return array with group names
984
+     *
985
+     * Returns a list with all groups
986
+     * Uses a paged search if available to override a
987
+     * server side search limit.
988
+     * (active directory has a limit of 1000 by default)
989
+     * @throws Exception
990
+     */
991
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
992
+        if (!$this->enabled) {
993
+            return [];
994
+        }
995
+        $cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
996
+
997
+        //Check cache before driving unnecessary searches
998
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
+        if (!is_null($ldap_groups)) {
1000
+            return $ldap_groups;
1001
+        }
1002
+
1003
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004
+        // error. With a limit of 0, we get 0 results. So we pass null.
1005
+        if ($limit <= 0) {
1006
+            $limit = null;
1007
+        }
1008
+        $filter = $this->access->combineFilterWithAnd([
1009
+            $this->access->connection->ldapGroupFilter,
1010
+            $this->access->getFilterPartForGroupSearch($search)
1011
+        ]);
1012
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
1013
+            [$this->access->connection->ldapGroupDisplayName, 'dn'],
1014
+            $limit,
1015
+            $offset);
1016
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1017
+
1018
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
1019
+        return $ldap_groups;
1020
+    }
1021
+
1022
+    /**
1023
+     * check if a group exists
1024
+     *
1025
+     * @param string $gid
1026
+     * @return bool
1027
+     * @throws ServerNotAvailableException
1028
+     */
1029
+    public function groupExists($gid) {
1030
+        $groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1031
+        if (!is_null($groupExists)) {
1032
+            return (bool)$groupExists;
1033
+        }
1034
+
1035
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1036
+        //only, requires more checking.
1037
+        $dn = $this->access->groupname2dn($gid);
1038
+        if (!$dn) {
1039
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1040
+            return false;
1041
+        }
1042
+
1043
+        if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1044
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1045
+            return false;
1046
+        }
1047
+
1048
+        //if group really still exists, we will be able to read its objectClass
1049
+        if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1050
+            $this->access->connection->writeToCache('groupExists' . $gid, false);
1051
+            return false;
1052
+        }
1053
+
1054
+        $this->access->connection->writeToCache('groupExists' . $gid, true);
1055
+        return true;
1056
+    }
1057
+
1058
+    /**
1059
+     * @throws ServerNotAvailableException
1060
+     * @throws Exception
1061
+     */
1062
+    protected function filterValidGroups(array $listOfGroups): array {
1063
+        $validGroupDNs = [];
1064
+        foreach ($listOfGroups as $key => $item) {
1065
+            $dn = is_string($item) ? $item : $item['dn'][0];
1066
+            $gid = $this->access->dn2groupname($dn);
1067
+            if (!$gid) {
1068
+                continue;
1069
+            }
1070
+            if ($this->groupExists($gid)) {
1071
+                $validGroupDNs[$key] = $item;
1072
+            }
1073
+        }
1074
+        return $validGroupDNs;
1075
+    }
1076
+
1077
+    /**
1078
+     * Check if backend implements actions
1079
+     *
1080
+     * @param int $actions bitwise-or'ed actions
1081
+     * @return boolean
1082
+     *
1083
+     * Returns the supported actions as int to be
1084
+     * compared with GroupInterface::CREATE_GROUP etc.
1085
+     */
1086
+    public function implementsActions($actions) {
1087
+        return (bool)((GroupInterface::COUNT_USERS |
1088
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1089
+    }
1090
+
1091
+    /**
1092
+     * Return access for LDAP interaction.
1093
+     *
1094
+     * @return Access instance of Access for LDAP interaction
1095
+     */
1096
+    public function getLDAPAccess($gid) {
1097
+        return $this->access;
1098
+    }
1099
+
1100
+    /**
1101
+     * create a group
1102
+     *
1103
+     * @param string $gid
1104
+     * @return bool
1105
+     * @throws Exception
1106
+     * @throws ServerNotAvailableException
1107
+     */
1108
+    public function createGroup($gid) {
1109
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1110
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1111
+                //updates group mapping
1112
+                $uuid = $this->access->getUUID($dn, false);
1113
+                if (is_string($uuid)) {
1114
+                    $this->access->mapAndAnnounceIfApplicable(
1115
+                        $this->access->getGroupMapper(),
1116
+                        $dn,
1117
+                        $gid,
1118
+                        $uuid,
1119
+                        false
1120
+                    );
1121
+                    $this->access->cacheGroupExists($gid);
1122
+                }
1123
+            }
1124
+            return $dn != null;
1125
+        }
1126
+        throw new Exception('Could not create group in LDAP backend.');
1127
+    }
1128
+
1129
+    /**
1130
+     * delete a group
1131
+     *
1132
+     * @param string $gid gid of the group to delete
1133
+     * @return bool
1134
+     * @throws Exception
1135
+     */
1136
+    public function deleteGroup($gid) {
1137
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1138
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1139
+                #delete group in nextcloud internal db
1140
+                $this->access->getGroupMapper()->unmap($gid);
1141
+                $this->access->connection->writeToCache("groupExists" . $gid, false);
1142
+            }
1143
+            return $ret;
1144
+        }
1145
+        throw new Exception('Could not delete group in LDAP backend.');
1146
+    }
1147
+
1148
+    /**
1149
+     * Add a user to a group
1150
+     *
1151
+     * @param string $uid Name of the user to add to group
1152
+     * @param string $gid Name of the group in which add the user
1153
+     * @return bool
1154
+     * @throws Exception
1155
+     */
1156
+    public function addToGroup($uid, $gid) {
1157
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1158
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1159
+                $this->access->connection->clearCache();
1160
+                unset($this->cachedGroupMembers[$gid]);
1161
+            }
1162
+            return $ret;
1163
+        }
1164
+        throw new Exception('Could not add user to group in LDAP backend.');
1165
+    }
1166
+
1167
+    /**
1168
+     * Removes a user from a group
1169
+     *
1170
+     * @param string $uid Name of the user to remove from group
1171
+     * @param string $gid Name of the group from which remove the user
1172
+     * @return bool
1173
+     * @throws Exception
1174
+     */
1175
+    public function removeFromGroup($uid, $gid) {
1176
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1177
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1178
+                $this->access->connection->clearCache();
1179
+                unset($this->cachedGroupMembers[$gid]);
1180
+            }
1181
+            return $ret;
1182
+        }
1183
+        throw new Exception('Could not remove user from group in LDAP backend.');
1184
+    }
1185
+
1186
+    /**
1187
+     * Gets group details
1188
+     *
1189
+     * @param string $gid Name of the group
1190
+     * @return array|false
1191
+     * @throws Exception
1192
+     */
1193
+    public function getGroupDetails($gid) {
1194
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1195
+            return $this->groupPluginManager->getGroupDetails($gid);
1196
+        }
1197
+        throw new Exception('Could not get group details in LDAP backend.');
1198
+    }
1199
+
1200
+    /**
1201
+     * Return LDAP connection resource from a cloned connection.
1202
+     * The cloned connection needs to be closed manually.
1203
+     * of the current access.
1204
+     *
1205
+     * @param string $gid
1206
+     * @return resource of the LDAP connection
1207
+     * @throws ServerNotAvailableException
1208
+     */
1209
+    public function getNewLDAPConnection($gid) {
1210
+        $connection = clone $this->access->getConnection();
1211
+        return $connection->getConnectionResource();
1212
+    }
1213
+
1214
+    /**
1215
+     * @throws ServerNotAvailableException
1216
+     */
1217
+    public function getDisplayName(string $gid): string {
1218
+        if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1219
+            return $this->groupPluginManager->getDisplayName($gid);
1220
+        }
1221
+
1222
+        $cacheKey = 'group_getDisplayName' . $gid;
1223
+        if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1224
+            return $displayName;
1225
+        }
1226
+
1227
+        $displayName = $this->access->readAttribute(
1228
+            $this->access->groupname2dn($gid),
1229
+            $this->access->connection->ldapGroupDisplayName);
1230
+
1231
+        if ($displayName && (count($displayName) > 0)) {
1232
+            $displayName = $displayName[0];
1233
+            $this->access->connection->writeToCache($cacheKey, $displayName);
1234
+            return $displayName;
1235
+        }
1236
+
1237
+        return '';
1238
+    }
1239 1239
 }
Please login to merge, or discard this patch.
Spacing   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -94,10 +94,10 @@  discard block
 block discarded – undo
94 94
 		if (!$this->enabled) {
95 95
 			return false;
96 96
 		}
97
-		$cacheKey = 'inGroup' . $uid . ':' . $gid;
97
+		$cacheKey = 'inGroup'.$uid.':'.$gid;
98 98
 		$inGroup = $this->access->connection->getFromCache($cacheKey);
99 99
 		if (!is_null($inGroup)) {
100
-			return (bool)$inGroup;
100
+			return (bool) $inGroup;
101 101
 		}
102 102
 
103 103
 		$userDN = $this->access->username2dn($uid);
@@ -106,7 +106,7 @@  discard block
 block discarded – undo
106 106
 			return in_array($userDN, $this->cachedGroupMembers[$gid]);
107 107
 		}
108 108
 
109
-		$cacheKeyMembers = 'inGroup-members:' . $gid;
109
+		$cacheKeyMembers = 'inGroup-members:'.$gid;
110 110
 		$members = $this->access->connection->getFromCache($cacheKeyMembers);
111 111
 		if (!is_null($members)) {
112 112
 			$this->cachedGroupMembers[$gid] = $members;
@@ -227,7 +227,7 @@  discard block
 block discarded – undo
227 227
 			return [];
228 228
 		}
229 229
 		// used extensively in cron job, caching makes sense for nested groups
230
-		$cacheKey = '_groupMembers' . $dnGroup;
230
+		$cacheKey = '_groupMembers'.$dnGroup;
231 231
 		$groupMembers = $this->access->connection->getFromCache($cacheKey);
232 232
 		if ($groupMembers !== null) {
233 233
 			return $groupMembers;
@@ -235,7 +235,7 @@  discard block
 block discarded – undo
235 235
 		$seen[$dnGroup] = 1;
236 236
 		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
237 237
 		if (is_array($members)) {
238
-			$fetcher = function ($memberDN, &$seen) {
238
+			$fetcher = function($memberDN, &$seen) {
239 239
 				return $this->_groupMembers($memberDN, $seen);
240 240
 			};
241 241
 			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
@@ -256,7 +256,7 @@  discard block
 block discarded – undo
256 256
 			return [];
257 257
 		}
258 258
 
259
-		$fetcher = function ($groupDN) {
259
+		$fetcher = function($groupDN) {
260 260
 			if (isset($this->cachedNestedGroups[$groupDN])) {
261 261
 				$nestedGroups = $this->cachedNestedGroups[$groupDN];
262 262
 			} else {
@@ -274,7 +274,7 @@  discard block
 block discarded – undo
274 274
 	}
275 275
 
276 276
 	private function walkNestedGroups(string $dn, Closure $fetcher, array $list): array {
277
-		$nesting = (int)$this->access->connection->ldapNestedGroups;
277
+		$nesting = (int) $this->access->connection->ldapNestedGroups;
278 278
 		// depending on the input, we either have a list of DNs or a list of LDAP records
279 279
 		// also, the output expects either DNs or records. Testing the first element should suffice.
280 280
 		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
@@ -282,7 +282,7 @@  discard block
 block discarded – undo
282 282
 		if ($nesting !== 1) {
283 283
 			if ($recordMode) {
284 284
 				// the keys are numeric, but should hold the DN
285
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
285
+				return array_reduce($list, function($transformed, $record) use ($dn) {
286 286
 					if ($record['dn'][0] != $dn) {
287 287
 						$transformed[$record['dn'][0]] = $record;
288 288
 					}
@@ -315,7 +315,7 @@  discard block
 block discarded – undo
315 315
 	 * @throws ServerNotAvailableException
316 316
 	 */
317 317
 	public function gidNumber2Name(string $gid, string $dn) {
318
-		$cacheKey = 'gidNumberToName' . $gid;
318
+		$cacheKey = 'gidNumberToName'.$gid;
319 319
 		$groupName = $this->access->connection->getFromCache($cacheKey);
320 320
 		if (!is_null($groupName) && isset($groupName)) {
321 321
 			return $groupName;
@@ -325,7 +325,7 @@  discard block
 block discarded – undo
325 325
 		$filter = $this->access->combineFilterWithAnd([
326 326
 			$this->access->connection->ldapGroupFilter,
327 327
 			'objectClass=posixGroup',
328
-			$this->access->connection->ldapGidNumber . '=' . $gid
328
+			$this->access->connection->ldapGidNumber.'='.$gid
329 329
 		]);
330 330
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
331 331
 	}
@@ -405,7 +405,7 @@  discard block
 block discarded – undo
405 405
 		if ($search !== '') {
406 406
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
407 407
 		}
408
-		$filterParts[] = $this->access->connection->ldapGidNumber . '=' . $groupID;
408
+		$filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID;
409 409
 
410 410
 		return $this->access->combineFilterWithAnd($filterParts);
411 411
 	}
@@ -475,7 +475,7 @@  discard block
 block discarded – undo
475 475
 		//we need to get the DN from LDAP
476 476
 		$filter = $this->access->combineFilterWithAnd([
477 477
 			$this->access->connection->ldapGroupFilter,
478
-			'objectsid=' . $domainObjectSid . '-' . $gid
478
+			'objectsid='.$domainObjectSid.'-'.$gid
479 479
 		]);
480 480
 		return $this->getNameOfGroup($filter, $cacheKey) ?? false;
481 481
 	}
@@ -532,7 +532,7 @@  discard block
 block discarded – undo
532 532
 		if ($search !== '') {
533 533
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
534 534
 		}
535
-		$filterParts[] = 'primaryGroupID=' . $groupID;
535
+		$filterParts[] = 'primaryGroupID='.$groupID;
536 536
 
537 537
 		return $this->access->combineFilterWithAnd($filterParts);
538 538
 	}
@@ -574,7 +574,7 @@  discard block
 block discarded – undo
574 574
 		try {
575 575
 			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576 576
 			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
577
-			return (int)$users;
577
+			return (int) $users;
578 578
 		} catch (ServerNotAvailableException $e) {
579 579
 			throw $e;
580 580
 		} catch (Exception $e) {
@@ -613,7 +613,7 @@  discard block
 block discarded – undo
613 613
 		if (!$this->enabled) {
614 614
 			return [];
615 615
 		}
616
-		$cacheKey = 'getUserGroups' . $uid;
616
+		$cacheKey = 'getUserGroups'.$uid;
617 617
 		$userGroups = $this->access->connection->getFromCache($cacheKey);
618 618
 		if (!is_null($userGroups)) {
619 619
 			return $userGroups;
@@ -671,8 +671,8 @@  discard block
 block discarded – undo
671 671
 		// if possible, read out membership via memberOf. It's far faster than
672 672
 		// performing a search, which still is a fallback later.
673 673
 		// memberof doesn't support memberuid, so skip it here.
674
-		if ((int)$this->access->connection->hasMemberOfFilterSupport === 1
675
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
674
+		if ((int) $this->access->connection->hasMemberOfFilterSupport === 1
675
+			&& (int) $this->access->connection->useMemberOfToDetectMembership === 1
676 676
 			&& strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
677 677
 		) {
678 678
 			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
@@ -758,11 +758,11 @@  discard block
 block discarded – undo
758 758
 		}
759 759
 		$allGroups = [];
760 760
 		$seen[$dn] = true;
761
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr . '=' . $dn;
761
+		$filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
762 762
 		$groups = $this->access->fetchListOfGroups($filter,
763 763
 			[strtolower($this->access->connection->ldapGroupMemberAssocAttr), $this->access->connection->ldapGroupDisplayName, 'dn']);
764 764
 		if (is_array($groups)) {
765
-			$fetcher = function ($dn, &$seen) {
765
+			$fetcher = function($dn, &$seen) {
766 766
 				if (is_array($dn) && isset($dn['dn'][0])) {
767 767
 					$dn = $dn['dn'][0];
768 768
 				}
@@ -793,7 +793,7 @@  discard block
 block discarded – undo
793 793
 			return [];
794 794
 		}
795 795
 		$search = $this->access->escapeFilterPart($search, true);
796
-		$cacheKey = 'usersInGroup-' . $gid . '-' . $search . '-' . $limit . '-' . $offset;
796
+		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
797 797
 		// check for cache of the exact query
798 798
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
799 799
 		if (!is_null($groupUsers)) {
@@ -804,7 +804,7 @@  discard block
 block discarded – undo
804 804
 			$limit = null;
805 805
 		}
806 806
 		// check for cache of the query without limit and offset
807
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-' . $gid . '-' . $search);
807
+		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
808 808
 		if (!is_null($groupUsers)) {
809 809
 			$groupUsers = array_slice($groupUsers, $offset, $limit);
810 810
 			$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -852,7 +852,7 @@  discard block
 block discarded – undo
852 852
 					continue;
853 853
 				}
854 854
 
855
-				$cacheKey = 'userExistsOnLDAP' . $uid;
855
+				$cacheKey = 'userExistsOnLDAP'.$uid;
856 856
 				$userExists = $this->access->connection->getFromCache($cacheKey);
857 857
 				if ($userExists === false) {
858 858
 					continue;
@@ -877,7 +877,7 @@  discard block
 block discarded – undo
877 877
 
878 878
 		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
879 879
 		natsort($groupUsers);
880
-		$this->access->connection->writeToCache('usersInGroup-' . $gid . '-' . $search, $groupUsers);
880
+		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
881 881
 		$groupUsers = array_slice($groupUsers, $offset, $limit);
882 882
 
883 883
 		$this->access->connection->writeToCache($cacheKey, $groupUsers);
@@ -899,7 +899,7 @@  discard block
 block discarded – undo
899 899
 			return $this->groupPluginManager->countUsersInGroup($gid, $search);
900 900
 		}
901 901
 
902
-		$cacheKey = 'countUsersInGroup-' . $gid . '-' . $search;
902
+		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
903 903
 		if (!$this->enabled || !$this->groupExists($gid)) {
904 904
 			return false;
905 905
 		}
@@ -992,7 +992,7 @@  discard block
 block discarded – undo
992 992
 		if (!$this->enabled) {
993 993
 			return [];
994 994
 		}
995
-		$cacheKey = 'getGroups-' . $search . '-' . $limit . '-' . $offset;
995
+		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
996 996
 
997 997
 		//Check cache before driving unnecessary searches
998 998
 		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
@@ -1027,31 +1027,31 @@  discard block
 block discarded – undo
1027 1027
 	 * @throws ServerNotAvailableException
1028 1028
 	 */
1029 1029
 	public function groupExists($gid) {
1030
-		$groupExists = $this->access->connection->getFromCache('groupExists' . $gid);
1030
+		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1031 1031
 		if (!is_null($groupExists)) {
1032
-			return (bool)$groupExists;
1032
+			return (bool) $groupExists;
1033 1033
 		}
1034 1034
 
1035 1035
 		//getting dn, if false the group does not exist. If dn, it may be mapped
1036 1036
 		//only, requires more checking.
1037 1037
 		$dn = $this->access->groupname2dn($gid);
1038 1038
 		if (!$dn) {
1039
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1039
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1040 1040
 			return false;
1041 1041
 		}
1042 1042
 
1043 1043
 		if (!$this->access->isDNPartOfBase($dn, $this->access->connection->ldapBaseGroups)) {
1044
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1044
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1045 1045
 			return false;
1046 1046
 		}
1047 1047
 
1048 1048
 		//if group really still exists, we will be able to read its objectClass
1049 1049
 		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapGroupFilter))) {
1050
-			$this->access->connection->writeToCache('groupExists' . $gid, false);
1050
+			$this->access->connection->writeToCache('groupExists'.$gid, false);
1051 1051
 			return false;
1052 1052
 		}
1053 1053
 
1054
-		$this->access->connection->writeToCache('groupExists' . $gid, true);
1054
+		$this->access->connection->writeToCache('groupExists'.$gid, true);
1055 1055
 		return true;
1056 1056
 	}
1057 1057
 
@@ -1084,7 +1084,7 @@  discard block
 block discarded – undo
1084 1084
 	 * compared with GroupInterface::CREATE_GROUP etc.
1085 1085
 	 */
1086 1086
 	public function implementsActions($actions) {
1087
-		return (bool)((GroupInterface::COUNT_USERS |
1087
+		return (bool) ((GroupInterface::COUNT_USERS |
1088 1088
 				$this->groupPluginManager->getImplementedActions()) & $actions);
1089 1089
 	}
1090 1090
 
@@ -1138,7 +1138,7 @@  discard block
 block discarded – undo
1138 1138
 			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1139 1139
 				#delete group in nextcloud internal db
1140 1140
 				$this->access->getGroupMapper()->unmap($gid);
1141
-				$this->access->connection->writeToCache("groupExists" . $gid, false);
1141
+				$this->access->connection->writeToCache("groupExists".$gid, false);
1142 1142
 			}
1143 1143
 			return $ret;
1144 1144
 		}
@@ -1219,7 +1219,7 @@  discard block
 block discarded – undo
1219 1219
 			return $this->groupPluginManager->getDisplayName($gid);
1220 1220
 		}
1221 1221
 
1222
-		$cacheKey = 'group_getDisplayName' . $gid;
1222
+		$cacheKey = 'group_getDisplayName'.$gid;
1223 1223
 		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1224 1224
 			return $displayName;
1225 1225
 		}
Please login to merge, or discard this patch.