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