Completed
Push — master ( fb38ec...86d503 )
by
unknown
38:05 queued 15:21
created
lib/private/User/Manager.php 1 patch
Indentation   +785 added lines, -785 removed lines patch added patch discarded remove patch
@@ -53,789 +53,789 @@
 block discarded – undo
53 53
  * @package OC\User
54 54
  */
55 55
 class Manager extends PublicEmitter implements IUserManager {
56
-	/**
57
-	 * @var UserInterface[] $backends
58
-	 */
59
-	private array $backends = [];
60
-
61
-	/**
62
-	 * @var array<string,\OC\User\User> $cachedUsers
63
-	 */
64
-	private array $cachedUsers = [];
65
-
66
-	private ICache $cache;
67
-
68
-	private DisplayNameCache $displayNameCache;
69
-
70
-	public function __construct(
71
-		private IConfig $config,
72
-		ICacheFactory $cacheFactory,
73
-		private IEventDispatcher $eventDispatcher,
74
-		private LoggerInterface $logger,
75
-	) {
76
-		$this->cache = new WithLocalCache($cacheFactory->createDistributed('user_backend_map'));
77
-		$this->listen('\OC\User', 'postDelete', function (IUser $user): void {
78
-			unset($this->cachedUsers[$user->getUID()]);
79
-		});
80
-		$this->displayNameCache = new DisplayNameCache($cacheFactory, $this);
81
-	}
82
-
83
-	/**
84
-	 * Get the active backends
85
-	 * @return UserInterface[]
86
-	 */
87
-	public function getBackends(): array {
88
-		return $this->backends;
89
-	}
90
-
91
-	public function registerBackend(UserInterface $backend): void {
92
-		$this->backends[] = $backend;
93
-	}
94
-
95
-	public function removeBackend(UserInterface $backend): void {
96
-		$this->cachedUsers = [];
97
-		if (($i = array_search($backend, $this->backends)) !== false) {
98
-			unset($this->backends[$i]);
99
-		}
100
-	}
101
-
102
-	public function clearBackends(): void {
103
-		$this->cachedUsers = [];
104
-		$this->backends = [];
105
-	}
106
-
107
-	/**
108
-	 * get a user by user id
109
-	 *
110
-	 * @param string $uid
111
-	 * @return \OC\User\User|null Either the user or null if the specified user does not exist
112
-	 */
113
-	public function get($uid) {
114
-		if (is_null($uid) || $uid === '' || $uid === false) {
115
-			return null;
116
-		}
117
-		if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
118
-			return $this->cachedUsers[$uid];
119
-		}
120
-
121
-		if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
122
-			return null;
123
-		}
124
-
125
-		$cachedBackend = $this->cache->get(sha1($uid));
126
-		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
127
-			// Cache has the info of the user backend already, so ask that one directly
128
-			$backend = $this->backends[$cachedBackend];
129
-			if ($backend->userExists($uid)) {
130
-				return $this->getUserObject($uid, $backend);
131
-			}
132
-		}
133
-
134
-		foreach ($this->backends as $i => $backend) {
135
-			if ($i === $cachedBackend) {
136
-				// Tried that one already
137
-				continue;
138
-			}
139
-
140
-			if ($backend->userExists($uid)) {
141
-				// Hash $uid to ensure that only valid characters are used for the cache key
142
-				$this->cache->set(sha1($uid), $i, 300);
143
-				return $this->getUserObject($uid, $backend);
144
-			}
145
-		}
146
-		return null;
147
-	}
148
-
149
-	public function getDisplayName(string $uid): ?string {
150
-		return $this->displayNameCache->getDisplayName($uid);
151
-	}
152
-
153
-	/**
154
-	 * get or construct the user object
155
-	 *
156
-	 * @param string $uid
157
-	 * @param \OCP\UserInterface $backend
158
-	 * @param bool $cacheUser If false the newly created user object will not be cached
159
-	 * @return \OC\User\User
160
-	 */
161
-	public function getUserObject($uid, $backend, $cacheUser = true) {
162
-		if ($backend instanceof IGetRealUIDBackend) {
163
-			$uid = $backend->getRealUID($uid);
164
-		}
165
-
166
-		if (isset($this->cachedUsers[$uid])) {
167
-			return $this->cachedUsers[$uid];
168
-		}
169
-
170
-		$user = new User($uid, $backend, $this->eventDispatcher, $this, $this->config);
171
-		if ($cacheUser) {
172
-			$this->cachedUsers[$uid] = $user;
173
-		}
174
-		return $user;
175
-	}
176
-
177
-	/**
178
-	 * check if a user exists
179
-	 *
180
-	 * @param string $uid
181
-	 * @return bool
182
-	 */
183
-	public function userExists($uid) {
184
-		if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
185
-			return false;
186
-		}
187
-
188
-		$user = $this->get($uid);
189
-		return ($user !== null);
190
-	}
191
-
192
-	/**
193
-	 * Check if the password is valid for the user
194
-	 *
195
-	 * @param string $loginName
196
-	 * @param string $password
197
-	 * @return IUser|false the User object on success, false otherwise
198
-	 */
199
-	public function checkPassword($loginName, $password) {
200
-		$result = $this->checkPasswordNoLogging($loginName, $password);
201
-
202
-		if ($result === false) {
203
-			$this->logger->warning('Login failed: \'' . $loginName . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
204
-		}
205
-
206
-		return $result;
207
-	}
208
-
209
-	/**
210
-	 * Check if the password is valid for the user
211
-	 *
212
-	 * @internal
213
-	 * @param string $loginName
214
-	 * @param string $password
215
-	 * @return IUser|false the User object on success, false otherwise
216
-	 */
217
-	public function checkPasswordNoLogging($loginName, $password) {
218
-		$loginName = str_replace("\0", '', $loginName);
219
-		$password = str_replace("\0", '', $password);
220
-
221
-		$cachedBackend = $this->cache->get($loginName);
222
-		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
223
-			$backends = [$this->backends[$cachedBackend]];
224
-		} else {
225
-			$backends = $this->backends;
226
-		}
227
-		foreach ($backends as $backend) {
228
-			if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
229
-				/** @var ICheckPasswordBackend $backend */
230
-				$uid = $backend->checkPassword($loginName, $password);
231
-				if ($uid !== false) {
232
-					return $this->getUserObject($uid, $backend);
233
-				}
234
-			}
235
-		}
236
-
237
-		// since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
238
-		// we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
239
-		// to contain urlencoded patterns by "accident".
240
-		$password = urldecode($password);
241
-
242
-		foreach ($backends as $backend) {
243
-			if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
244
-				/** @var ICheckPasswordBackend|UserInterface $backend */
245
-				$uid = $backend->checkPassword($loginName, $password);
246
-				if ($uid !== false) {
247
-					return $this->getUserObject($uid, $backend);
248
-				}
249
-			}
250
-		}
251
-
252
-		return false;
253
-	}
254
-
255
-	/**
256
-	 * Search by user id
257
-	 *
258
-	 * @param string $pattern
259
-	 * @param int $limit
260
-	 * @param int $offset
261
-	 * @return IUser[]
262
-	 * @deprecated 27.0.0, use searchDisplayName instead
263
-	 */
264
-	public function search($pattern, $limit = null, $offset = null) {
265
-		$users = [];
266
-		foreach ($this->backends as $backend) {
267
-			$backendUsers = $backend->getUsers($pattern, $limit, $offset);
268
-			if (is_array($backendUsers)) {
269
-				foreach ($backendUsers as $uid) {
270
-					$users[$uid] = new LazyUser($uid, $this, null, $backend);
271
-				}
272
-			}
273
-		}
274
-
275
-		uasort($users, function (IUser $a, IUser $b) {
276
-			return strcasecmp($a->getUID(), $b->getUID());
277
-		});
278
-		return $users;
279
-	}
280
-
281
-	/**
282
-	 * Search by displayName
283
-	 *
284
-	 * @param string $pattern
285
-	 * @param int $limit
286
-	 * @param int $offset
287
-	 * @return IUser[]
288
-	 */
289
-	public function searchDisplayName($pattern, $limit = null, $offset = null) {
290
-		$users = [];
291
-		foreach ($this->backends as $backend) {
292
-			$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
293
-			if (is_array($backendUsers)) {
294
-				foreach ($backendUsers as $uid => $displayName) {
295
-					$users[] = new LazyUser($uid, $this, $displayName, $backend);
296
-				}
297
-			}
298
-		}
299
-
300
-		usort($users, function (IUser $a, IUser $b) {
301
-			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
302
-		});
303
-		return $users;
304
-	}
305
-
306
-	/**
307
-	 * @return IUser[]
308
-	 */
309
-	public function getDisabledUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
310
-		$users = $this->config->getUsersForUserValue('core', 'enabled', 'false');
311
-		$users = array_combine(
312
-			$users,
313
-			array_map(
314
-				fn (string $uid): IUser => new LazyUser($uid, $this),
315
-				$users
316
-			)
317
-		);
318
-		if ($search !== '') {
319
-			$users = array_filter(
320
-				$users,
321
-				function (IUser $user) use ($search): bool {
322
-					try {
323
-						return mb_stripos($user->getUID(), $search) !== false ||
324
-						mb_stripos($user->getDisplayName(), $search) !== false ||
325
-						mb_stripos($user->getEMailAddress() ?? '', $search) !== false;
326
-					} catch (NoUserException $ex) {
327
-						$this->logger->error('Error while filtering disabled users', ['exception' => $ex, 'userUID' => $user->getUID()]);
328
-						return false;
329
-					}
330
-				});
331
-		}
332
-
333
-		$tempLimit = ($limit === null ? null : $limit + $offset);
334
-		foreach ($this->backends as $backend) {
335
-			if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
336
-				break;
337
-			}
338
-			if ($backend instanceof IProvideEnabledStateBackend) {
339
-				$backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)), 0, $search);
340
-				foreach ($backendUsers as $uid) {
341
-					$users[$uid] = new LazyUser($uid, $this, null, $backend);
342
-				}
343
-			}
344
-		}
345
-
346
-		return array_slice($users, $offset, $limit);
347
-	}
348
-
349
-	/**
350
-	 * Search known users (from phonebook sync) by displayName
351
-	 *
352
-	 * @param string $searcher
353
-	 * @param string $pattern
354
-	 * @param int|null $limit
355
-	 * @param int|null $offset
356
-	 * @return IUser[]
357
-	 */
358
-	public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
359
-		$users = [];
360
-		foreach ($this->backends as $backend) {
361
-			if ($backend instanceof ISearchKnownUsersBackend) {
362
-				$backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset);
363
-			} else {
364
-				// Better than nothing, but filtering after pagination can remove lots of results.
365
-				$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
366
-			}
367
-			if (is_array($backendUsers)) {
368
-				foreach ($backendUsers as $uid => $displayName) {
369
-					$users[] = $this->getUserObject($uid, $backend);
370
-				}
371
-			}
372
-		}
373
-
374
-		usort($users, function ($a, $b) {
375
-			/**
376
-			 * @var IUser $a
377
-			 * @var IUser $b
378
-			 */
379
-			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
380
-		});
381
-		return $users;
382
-	}
383
-
384
-	/**
385
-	 * @param string $uid
386
-	 * @param string $password
387
-	 * @return false|IUser the created user or false
388
-	 * @throws \InvalidArgumentException
389
-	 * @throws HintException
390
-	 */
391
-	public function createUser($uid, $password) {
392
-		// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
393
-		/** @var IAssertion $assertion */
394
-		$assertion = \OC::$server->get(IAssertion::class);
395
-		$assertion->createUserIsLegit();
396
-
397
-		$localBackends = [];
398
-		foreach ($this->backends as $backend) {
399
-			if ($backend instanceof Database) {
400
-				// First check if there is another user backend
401
-				$localBackends[] = $backend;
402
-				continue;
403
-			}
404
-
405
-			if ($backend->implementsActions(Backend::CREATE_USER)) {
406
-				return $this->createUserFromBackend($uid, $password, $backend);
407
-			}
408
-		}
409
-
410
-		foreach ($localBackends as $backend) {
411
-			if ($backend->implementsActions(Backend::CREATE_USER)) {
412
-				return $this->createUserFromBackend($uid, $password, $backend);
413
-			}
414
-		}
415
-
416
-		return false;
417
-	}
418
-
419
-	/**
420
-	 * @param string $uid
421
-	 * @param string $password
422
-	 * @param UserInterface $backend
423
-	 * @return IUser|false
424
-	 * @throws \InvalidArgumentException
425
-	 */
426
-	public function createUserFromBackend($uid, $password, UserInterface $backend) {
427
-		$l = \OCP\Util::getL10N('lib');
428
-
429
-		$this->validateUserId($uid, true);
430
-
431
-		// No empty password
432
-		if (trim($password) === '') {
433
-			throw new \InvalidArgumentException($l->t('A valid password must be provided'));
434
-		}
435
-
436
-		// Check if user already exists
437
-		if ($this->userExists($uid)) {
438
-			throw new \InvalidArgumentException($l->t('The Login is already being used'));
439
-		}
440
-
441
-		/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
442
-		$this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
443
-		$this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
444
-		$state = $backend->createUser($uid, $password);
445
-		if ($state === false) {
446
-			throw new \InvalidArgumentException($l->t('Could not create account'));
447
-		}
448
-		$user = $this->getUserObject($uid, $backend);
449
-		if ($user instanceof IUser) {
450
-			/** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
451
-			$this->emit('\OC\User', 'postCreateUser', [$user, $password]);
452
-			$this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
453
-			return $user;
454
-		}
455
-		return false;
456
-	}
457
-
458
-	/**
459
-	 * returns how many users per backend exist (if supported by backend)
460
-	 *
461
-	 * @param boolean $hasLoggedIn when true only users that have a lastLogin
462
-	 *                             entry in the preferences table will be affected
463
-	 * @return array<string, int> an array of backend class as key and count number as value
464
-	 */
465
-	public function countUsers() {
466
-		$userCountStatistics = [];
467
-		foreach ($this->backends as $backend) {
468
-			if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
469
-				/** @var ICountUsersBackend|IUserBackend $backend */
470
-				$backendUsers = $backend->countUsers();
471
-				if ($backendUsers !== false) {
472
-					if ($backend instanceof IUserBackend) {
473
-						$name = $backend->getBackendName();
474
-					} else {
475
-						$name = get_class($backend);
476
-					}
477
-					if (isset($userCountStatistics[$name])) {
478
-						$userCountStatistics[$name] += $backendUsers;
479
-					} else {
480
-						$userCountStatistics[$name] = $backendUsers;
481
-					}
482
-				}
483
-			}
484
-		}
485
-		return $userCountStatistics;
486
-	}
487
-
488
-	public function countUsersTotal(int $limit = 0, bool $onlyMappedUsers = false): int|false {
489
-		$userCount = false;
490
-
491
-		foreach ($this->backends as $backend) {
492
-			if ($onlyMappedUsers && $backend instanceof ICountMappedUsersBackend) {
493
-				$backendUsers = $backend->countMappedUsers();
494
-			} elseif ($backend instanceof ILimitAwareCountUsersBackend) {
495
-				$backendUsers = $backend->countUsers($limit);
496
-			} elseif ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
497
-				/** @var ICountUsersBackend $backend */
498
-				$backendUsers = $backend->countUsers();
499
-			} else {
500
-				$this->logger->debug('Skip backend for user count: ' . get_class($backend));
501
-				continue;
502
-			}
503
-			if ($backendUsers !== false) {
504
-				$userCount = (int)$userCount + $backendUsers;
505
-				if ($limit > 0) {
506
-					if ($userCount >= $limit) {
507
-						break;
508
-					}
509
-					$limit -= $userCount;
510
-				}
511
-			} else {
512
-				$this->logger->warning('Can not determine user count for ' . get_class($backend));
513
-			}
514
-		}
515
-		return $userCount;
516
-	}
517
-
518
-	/**
519
-	 * returns how many users per backend exist in the requested groups (if supported by backend)
520
-	 *
521
-	 * @param IGroup[] $groups an array of groups to search in
522
-	 * @param int $limit limit to stop counting
523
-	 * @return array{int,int} total number of users, and number of disabled users in the given groups, below $limit. If limit is reached, -1 is returned for number of disabled users
524
-	 */
525
-	public function countUsersAndDisabledUsersOfGroups(array $groups, int $limit): array {
526
-		$users = [];
527
-		$disabled = [];
528
-		foreach ($groups as $group) {
529
-			foreach ($group->getUsers() as $user) {
530
-				$users[$user->getUID()] = 1;
531
-				if (!$user->isEnabled()) {
532
-					$disabled[$user->getUID()] = 1;
533
-				}
534
-				if (count($users) >= $limit) {
535
-					return [count($users),-1];
536
-				}
537
-			}
538
-		}
539
-		return [count($users),count($disabled)];
540
-	}
541
-
542
-	/**
543
-	 * The callback is executed for each user on each backend.
544
-	 * If the callback returns false no further users will be retrieved.
545
-	 *
546
-	 * @psalm-param \Closure(\OCP\IUser):?bool $callback
547
-	 * @param string $search
548
-	 * @param boolean $onlySeen when true only users that have a lastLogin entry
549
-	 *                          in the preferences table will be affected
550
-	 * @since 9.0.0
551
-	 */
552
-	public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
553
-		if ($onlySeen) {
554
-			$this->callForSeenUsers($callback);
555
-		} else {
556
-			foreach ($this->getBackends() as $backend) {
557
-				$limit = 500;
558
-				$offset = 0;
559
-				do {
560
-					$users = $backend->getUsers($search, $limit, $offset);
561
-					foreach ($users as $uid) {
562
-						if (!$backend->userExists($uid)) {
563
-							continue;
564
-						}
565
-						$user = $this->getUserObject($uid, $backend, false);
566
-						$return = $callback($user);
567
-						if ($return === false) {
568
-							break;
569
-						}
570
-					}
571
-					$offset += $limit;
572
-				} while (count($users) >= $limit);
573
-			}
574
-		}
575
-	}
576
-
577
-	/**
578
-	 * returns how many users are disabled
579
-	 *
580
-	 * @return int
581
-	 * @since 12.0.0
582
-	 */
583
-	public function countDisabledUsers(): int {
584
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
585
-		$queryBuilder->select($queryBuilder->func()->count('*'))
586
-			->from('preferences')
587
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
588
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
589
-			->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));
590
-
591
-
592
-		$result = $queryBuilder->execute();
593
-		$count = $result->fetchOne();
594
-		$result->closeCursor();
595
-
596
-		if ($count !== false) {
597
-			$count = (int)$count;
598
-		} else {
599
-			$count = 0;
600
-		}
601
-
602
-		return $count;
603
-	}
604
-
605
-	/**
606
-	 * returns how many users have logged in once
607
-	 *
608
-	 * @return int
609
-	 * @since 11.0.0
610
-	 */
611
-	public function countSeenUsers() {
612
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
613
-		$queryBuilder->select($queryBuilder->func()->count('*'))
614
-			->from('preferences')
615
-			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
616
-			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')));
617
-
618
-		$query = $queryBuilder->execute();
619
-
620
-		$result = (int)$query->fetchOne();
621
-		$query->closeCursor();
622
-
623
-		return $result;
624
-	}
625
-
626
-	public function callForSeenUsers(\Closure $callback) {
627
-		$users = $this->getSeenUsers();
628
-		foreach ($users as $user) {
629
-			$return = $callback($user);
630
-			if ($return === false) {
631
-				return;
632
-			}
633
-		}
634
-	}
635
-
636
-	/**
637
-	 * Getting all userIds that have a listLogin value requires checking the
638
-	 * value in php because on oracle you cannot use a clob in a where clause,
639
-	 * preventing us from doing a not null or length(value) > 0 check.
640
-	 *
641
-	 * @param int $limit
642
-	 * @param int $offset
643
-	 * @return string[] with user ids
644
-	 */
645
-	private function getSeenUserIds($limit = null, $offset = null) {
646
-		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
647
-		$queryBuilder->select(['userid'])
648
-			->from('preferences')
649
-			->where($queryBuilder->expr()->eq(
650
-				'appid', $queryBuilder->createNamedParameter('login'))
651
-			)
652
-			->andWhere($queryBuilder->expr()->eq(
653
-				'configkey', $queryBuilder->createNamedParameter('lastLogin'))
654
-			)
655
-			->andWhere($queryBuilder->expr()->isNotNull('configvalue')
656
-			);
657
-
658
-		if ($limit !== null) {
659
-			$queryBuilder->setMaxResults($limit);
660
-		}
661
-		if ($offset !== null) {
662
-			$queryBuilder->setFirstResult($offset);
663
-		}
664
-		$query = $queryBuilder->execute();
665
-		$result = [];
666
-
667
-		while ($row = $query->fetch()) {
668
-			$result[] = $row['userid'];
669
-		}
670
-
671
-		$query->closeCursor();
672
-
673
-		return $result;
674
-	}
675
-
676
-	/**
677
-	 * @param string $email
678
-	 * @return IUser[]
679
-	 * @since 9.1.0
680
-	 */
681
-	public function getByEmail($email) {
682
-		// looking for 'email' only (and not primary_mail) is intentional
683
-		$userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);
684
-
685
-		$users = array_map(function ($uid) {
686
-			return $this->get($uid);
687
-		}, $userIds);
688
-
689
-		return array_values(array_filter($users, function ($u) {
690
-			return ($u instanceof IUser);
691
-		}));
692
-	}
693
-
694
-	/**
695
-	 * @param string $uid
696
-	 * @param bool $checkDataDirectory
697
-	 * @throws \InvalidArgumentException Message is an already translated string with a reason why the id is not valid
698
-	 * @since 26.0.0
699
-	 */
700
-	public function validateUserId(string $uid, bool $checkDataDirectory = false): void {
701
-		$l = Server::get(IFactory::class)->get('lib');
702
-
703
-		// Check the ID for bad characters
704
-		// Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'"
705
-		if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
706
-			throw new \InvalidArgumentException($l->t('Only the following characters are allowed in an Login:'
707
-				. ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'));
708
-		}
709
-
710
-		// No empty user ID
711
-		if (trim($uid) === '') {
712
-			throw new \InvalidArgumentException($l->t('A valid Login must be provided'));
713
-		}
714
-
715
-		// No whitespace at the beginning or at the end
716
-		if (trim($uid) !== $uid) {
717
-			throw new \InvalidArgumentException($l->t('Login contains whitespace at the beginning or at the end'));
718
-		}
719
-
720
-		// User ID only consists of 1 or 2 dots (directory traversal)
721
-		if ($uid === '.' || $uid === '..') {
722
-			throw new \InvalidArgumentException($l->t('Login must not consist of dots only'));
723
-		}
724
-
725
-		// User ID is too long
726
-		if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
727
-		    // TRANSLATORS User ID is too long
728
-			throw new \InvalidArgumentException($l->t('Username is too long'));
729
-		}
730
-
731
-		if (!$this->verifyUid($uid, $checkDataDirectory)) {
732
-			throw new \InvalidArgumentException($l->t('Login is invalid because files already exist for this user'));
733
-		}
734
-	}
735
-
736
-	/**
737
-	 * Gets the list of user ids sorted by lastLogin, from most recent to least recent
738
-	 *
739
-	 * @param int|null $limit how many users to fetch (default: 25, max: 100)
740
-	 * @param int $offset from which offset to fetch
741
-	 * @param string $search search users based on search params
742
-	 * @return list<string> list of user IDs
743
-	 */
744
-	public function getLastLoggedInUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
745
-		// We can't load all users who already logged in
746
-		$limit = min(100, $limit ?: 25);
747
-
748
-		$connection = \OC::$server->getDatabaseConnection();
749
-		$queryBuilder = $connection->getQueryBuilder();
750
-		$queryBuilder->select('pref_login.userid')
751
-			->from('preferences', 'pref_login')
752
-			->where($queryBuilder->expr()->eq('pref_login.appid', $queryBuilder->expr()->literal('login')))
753
-			->andWhere($queryBuilder->expr()->eq('pref_login.configkey', $queryBuilder->expr()->literal('lastLogin')))
754
-			->setFirstResult($offset)
755
-			->setMaxResults($limit)
756
-		;
757
-
758
-		// Oracle don't want to run ORDER BY on CLOB column
759
-		$loginOrder = $connection->getDatabasePlatform() instanceof OraclePlatform
760
-			? $queryBuilder->expr()->castColumn('pref_login.configvalue', IQueryBuilder::PARAM_INT)
761
-			: 'pref_login.configvalue';
762
-		$queryBuilder
763
-			->orderBy($loginOrder, 'DESC')
764
-			->addOrderBy($queryBuilder->func()->lower('pref_login.userid'), 'ASC');
765
-
766
-		if ($search !== '') {
767
-			$displayNameMatches = $this->searchDisplayName($search);
768
-			$matchedUids = array_map(static fn (IUser $u): string => $u->getUID(), $displayNameMatches);
769
-
770
-			$queryBuilder
771
-				->leftJoin('pref_login', 'preferences', 'pref_email', $queryBuilder->expr()->andX(
772
-					$queryBuilder->expr()->eq('pref_login.userid', 'pref_email.userid'),
773
-					$queryBuilder->expr()->eq('pref_email.appid', $queryBuilder->expr()->literal('settings')),
774
-					$queryBuilder->expr()->eq('pref_email.configkey', $queryBuilder->expr()->literal('email')),
775
-				))
776
-				->andWhere($queryBuilder->expr()->orX(
777
-					$queryBuilder->expr()->in('pref_login.userid', $queryBuilder->createNamedParameter($matchedUids, IQueryBuilder::PARAM_STR_ARRAY)),
778
-				));
779
-		}
780
-
781
-		/** @var list<string> */
782
-		$list = $queryBuilder->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
783
-
784
-		return $list;
785
-	}
786
-
787
-	private function verifyUid(string $uid, bool $checkDataDirectory = false): bool {
788
-		$appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');
789
-
790
-		if (\in_array($uid, [
791
-			'.htaccess',
792
-			'files_external',
793
-			'__groupfolders',
794
-			'.ncdata',
795
-			'owncloud.log',
796
-			'nextcloud.log',
797
-			'updater.log',
798
-			'audit.log',
799
-			$appdata], true)) {
800
-			return false;
801
-		}
802
-
803
-		if (!$checkDataDirectory) {
804
-			return true;
805
-		}
806
-
807
-		$dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
808
-
809
-		return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
810
-	}
811
-
812
-	public function getDisplayNameCache(): DisplayNameCache {
813
-		return $this->displayNameCache;
814
-	}
815
-
816
-	/**
817
-	 * Gets the list of users sorted by lastLogin, from most recent to least recent
818
-	 *
819
-	 * @param int $offset from which offset to fetch
820
-	 * @return \Iterator<IUser> list of user IDs
821
-	 * @since 30.0.0
822
-	 */
823
-	public function getSeenUsers(int $offset = 0): \Iterator {
824
-		$limit = 1000;
825
-
826
-		do {
827
-			$userIds = $this->getSeenUserIds($limit, $offset);
828
-			$offset += $limit;
829
-
830
-			foreach ($userIds as $userId) {
831
-				foreach ($this->backends as $backend) {
832
-					if ($backend->userExists($userId)) {
833
-						$user = $this->getUserObject($userId, $backend, false);
834
-						yield $user;
835
-						break;
836
-					}
837
-				}
838
-			}
839
-		} while (count($userIds) === $limit);
840
-	}
56
+    /**
57
+     * @var UserInterface[] $backends
58
+     */
59
+    private array $backends = [];
60
+
61
+    /**
62
+     * @var array<string,\OC\User\User> $cachedUsers
63
+     */
64
+    private array $cachedUsers = [];
65
+
66
+    private ICache $cache;
67
+
68
+    private DisplayNameCache $displayNameCache;
69
+
70
+    public function __construct(
71
+        private IConfig $config,
72
+        ICacheFactory $cacheFactory,
73
+        private IEventDispatcher $eventDispatcher,
74
+        private LoggerInterface $logger,
75
+    ) {
76
+        $this->cache = new WithLocalCache($cacheFactory->createDistributed('user_backend_map'));
77
+        $this->listen('\OC\User', 'postDelete', function (IUser $user): void {
78
+            unset($this->cachedUsers[$user->getUID()]);
79
+        });
80
+        $this->displayNameCache = new DisplayNameCache($cacheFactory, $this);
81
+    }
82
+
83
+    /**
84
+     * Get the active backends
85
+     * @return UserInterface[]
86
+     */
87
+    public function getBackends(): array {
88
+        return $this->backends;
89
+    }
90
+
91
+    public function registerBackend(UserInterface $backend): void {
92
+        $this->backends[] = $backend;
93
+    }
94
+
95
+    public function removeBackend(UserInterface $backend): void {
96
+        $this->cachedUsers = [];
97
+        if (($i = array_search($backend, $this->backends)) !== false) {
98
+            unset($this->backends[$i]);
99
+        }
100
+    }
101
+
102
+    public function clearBackends(): void {
103
+        $this->cachedUsers = [];
104
+        $this->backends = [];
105
+    }
106
+
107
+    /**
108
+     * get a user by user id
109
+     *
110
+     * @param string $uid
111
+     * @return \OC\User\User|null Either the user or null if the specified user does not exist
112
+     */
113
+    public function get($uid) {
114
+        if (is_null($uid) || $uid === '' || $uid === false) {
115
+            return null;
116
+        }
117
+        if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
118
+            return $this->cachedUsers[$uid];
119
+        }
120
+
121
+        if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
122
+            return null;
123
+        }
124
+
125
+        $cachedBackend = $this->cache->get(sha1($uid));
126
+        if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
127
+            // Cache has the info of the user backend already, so ask that one directly
128
+            $backend = $this->backends[$cachedBackend];
129
+            if ($backend->userExists($uid)) {
130
+                return $this->getUserObject($uid, $backend);
131
+            }
132
+        }
133
+
134
+        foreach ($this->backends as $i => $backend) {
135
+            if ($i === $cachedBackend) {
136
+                // Tried that one already
137
+                continue;
138
+            }
139
+
140
+            if ($backend->userExists($uid)) {
141
+                // Hash $uid to ensure that only valid characters are used for the cache key
142
+                $this->cache->set(sha1($uid), $i, 300);
143
+                return $this->getUserObject($uid, $backend);
144
+            }
145
+        }
146
+        return null;
147
+    }
148
+
149
+    public function getDisplayName(string $uid): ?string {
150
+        return $this->displayNameCache->getDisplayName($uid);
151
+    }
152
+
153
+    /**
154
+     * get or construct the user object
155
+     *
156
+     * @param string $uid
157
+     * @param \OCP\UserInterface $backend
158
+     * @param bool $cacheUser If false the newly created user object will not be cached
159
+     * @return \OC\User\User
160
+     */
161
+    public function getUserObject($uid, $backend, $cacheUser = true) {
162
+        if ($backend instanceof IGetRealUIDBackend) {
163
+            $uid = $backend->getRealUID($uid);
164
+        }
165
+
166
+        if (isset($this->cachedUsers[$uid])) {
167
+            return $this->cachedUsers[$uid];
168
+        }
169
+
170
+        $user = new User($uid, $backend, $this->eventDispatcher, $this, $this->config);
171
+        if ($cacheUser) {
172
+            $this->cachedUsers[$uid] = $user;
173
+        }
174
+        return $user;
175
+    }
176
+
177
+    /**
178
+     * check if a user exists
179
+     *
180
+     * @param string $uid
181
+     * @return bool
182
+     */
183
+    public function userExists($uid) {
184
+        if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
185
+            return false;
186
+        }
187
+
188
+        $user = $this->get($uid);
189
+        return ($user !== null);
190
+    }
191
+
192
+    /**
193
+     * Check if the password is valid for the user
194
+     *
195
+     * @param string $loginName
196
+     * @param string $password
197
+     * @return IUser|false the User object on success, false otherwise
198
+     */
199
+    public function checkPassword($loginName, $password) {
200
+        $result = $this->checkPasswordNoLogging($loginName, $password);
201
+
202
+        if ($result === false) {
203
+            $this->logger->warning('Login failed: \'' . $loginName . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
204
+        }
205
+
206
+        return $result;
207
+    }
208
+
209
+    /**
210
+     * Check if the password is valid for the user
211
+     *
212
+     * @internal
213
+     * @param string $loginName
214
+     * @param string $password
215
+     * @return IUser|false the User object on success, false otherwise
216
+     */
217
+    public function checkPasswordNoLogging($loginName, $password) {
218
+        $loginName = str_replace("\0", '', $loginName);
219
+        $password = str_replace("\0", '', $password);
220
+
221
+        $cachedBackend = $this->cache->get($loginName);
222
+        if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
223
+            $backends = [$this->backends[$cachedBackend]];
224
+        } else {
225
+            $backends = $this->backends;
226
+        }
227
+        foreach ($backends as $backend) {
228
+            if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
229
+                /** @var ICheckPasswordBackend $backend */
230
+                $uid = $backend->checkPassword($loginName, $password);
231
+                if ($uid !== false) {
232
+                    return $this->getUserObject($uid, $backend);
233
+                }
234
+            }
235
+        }
236
+
237
+        // since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
238
+        // we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
239
+        // to contain urlencoded patterns by "accident".
240
+        $password = urldecode($password);
241
+
242
+        foreach ($backends as $backend) {
243
+            if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
244
+                /** @var ICheckPasswordBackend|UserInterface $backend */
245
+                $uid = $backend->checkPassword($loginName, $password);
246
+                if ($uid !== false) {
247
+                    return $this->getUserObject($uid, $backend);
248
+                }
249
+            }
250
+        }
251
+
252
+        return false;
253
+    }
254
+
255
+    /**
256
+     * Search by user id
257
+     *
258
+     * @param string $pattern
259
+     * @param int $limit
260
+     * @param int $offset
261
+     * @return IUser[]
262
+     * @deprecated 27.0.0, use searchDisplayName instead
263
+     */
264
+    public function search($pattern, $limit = null, $offset = null) {
265
+        $users = [];
266
+        foreach ($this->backends as $backend) {
267
+            $backendUsers = $backend->getUsers($pattern, $limit, $offset);
268
+            if (is_array($backendUsers)) {
269
+                foreach ($backendUsers as $uid) {
270
+                    $users[$uid] = new LazyUser($uid, $this, null, $backend);
271
+                }
272
+            }
273
+        }
274
+
275
+        uasort($users, function (IUser $a, IUser $b) {
276
+            return strcasecmp($a->getUID(), $b->getUID());
277
+        });
278
+        return $users;
279
+    }
280
+
281
+    /**
282
+     * Search by displayName
283
+     *
284
+     * @param string $pattern
285
+     * @param int $limit
286
+     * @param int $offset
287
+     * @return IUser[]
288
+     */
289
+    public function searchDisplayName($pattern, $limit = null, $offset = null) {
290
+        $users = [];
291
+        foreach ($this->backends as $backend) {
292
+            $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
293
+            if (is_array($backendUsers)) {
294
+                foreach ($backendUsers as $uid => $displayName) {
295
+                    $users[] = new LazyUser($uid, $this, $displayName, $backend);
296
+                }
297
+            }
298
+        }
299
+
300
+        usort($users, function (IUser $a, IUser $b) {
301
+            return strcasecmp($a->getDisplayName(), $b->getDisplayName());
302
+        });
303
+        return $users;
304
+    }
305
+
306
+    /**
307
+     * @return IUser[]
308
+     */
309
+    public function getDisabledUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
310
+        $users = $this->config->getUsersForUserValue('core', 'enabled', 'false');
311
+        $users = array_combine(
312
+            $users,
313
+            array_map(
314
+                fn (string $uid): IUser => new LazyUser($uid, $this),
315
+                $users
316
+            )
317
+        );
318
+        if ($search !== '') {
319
+            $users = array_filter(
320
+                $users,
321
+                function (IUser $user) use ($search): bool {
322
+                    try {
323
+                        return mb_stripos($user->getUID(), $search) !== false ||
324
+                        mb_stripos($user->getDisplayName(), $search) !== false ||
325
+                        mb_stripos($user->getEMailAddress() ?? '', $search) !== false;
326
+                    } catch (NoUserException $ex) {
327
+                        $this->logger->error('Error while filtering disabled users', ['exception' => $ex, 'userUID' => $user->getUID()]);
328
+                        return false;
329
+                    }
330
+                });
331
+        }
332
+
333
+        $tempLimit = ($limit === null ? null : $limit + $offset);
334
+        foreach ($this->backends as $backend) {
335
+            if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
336
+                break;
337
+            }
338
+            if ($backend instanceof IProvideEnabledStateBackend) {
339
+                $backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)), 0, $search);
340
+                foreach ($backendUsers as $uid) {
341
+                    $users[$uid] = new LazyUser($uid, $this, null, $backend);
342
+                }
343
+            }
344
+        }
345
+
346
+        return array_slice($users, $offset, $limit);
347
+    }
348
+
349
+    /**
350
+     * Search known users (from phonebook sync) by displayName
351
+     *
352
+     * @param string $searcher
353
+     * @param string $pattern
354
+     * @param int|null $limit
355
+     * @param int|null $offset
356
+     * @return IUser[]
357
+     */
358
+    public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
359
+        $users = [];
360
+        foreach ($this->backends as $backend) {
361
+            if ($backend instanceof ISearchKnownUsersBackend) {
362
+                $backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset);
363
+            } else {
364
+                // Better than nothing, but filtering after pagination can remove lots of results.
365
+                $backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
366
+            }
367
+            if (is_array($backendUsers)) {
368
+                foreach ($backendUsers as $uid => $displayName) {
369
+                    $users[] = $this->getUserObject($uid, $backend);
370
+                }
371
+            }
372
+        }
373
+
374
+        usort($users, function ($a, $b) {
375
+            /**
376
+             * @var IUser $a
377
+             * @var IUser $b
378
+             */
379
+            return strcasecmp($a->getDisplayName(), $b->getDisplayName());
380
+        });
381
+        return $users;
382
+    }
383
+
384
+    /**
385
+     * @param string $uid
386
+     * @param string $password
387
+     * @return false|IUser the created user or false
388
+     * @throws \InvalidArgumentException
389
+     * @throws HintException
390
+     */
391
+    public function createUser($uid, $password) {
392
+        // DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
393
+        /** @var IAssertion $assertion */
394
+        $assertion = \OC::$server->get(IAssertion::class);
395
+        $assertion->createUserIsLegit();
396
+
397
+        $localBackends = [];
398
+        foreach ($this->backends as $backend) {
399
+            if ($backend instanceof Database) {
400
+                // First check if there is another user backend
401
+                $localBackends[] = $backend;
402
+                continue;
403
+            }
404
+
405
+            if ($backend->implementsActions(Backend::CREATE_USER)) {
406
+                return $this->createUserFromBackend($uid, $password, $backend);
407
+            }
408
+        }
409
+
410
+        foreach ($localBackends as $backend) {
411
+            if ($backend->implementsActions(Backend::CREATE_USER)) {
412
+                return $this->createUserFromBackend($uid, $password, $backend);
413
+            }
414
+        }
415
+
416
+        return false;
417
+    }
418
+
419
+    /**
420
+     * @param string $uid
421
+     * @param string $password
422
+     * @param UserInterface $backend
423
+     * @return IUser|false
424
+     * @throws \InvalidArgumentException
425
+     */
426
+    public function createUserFromBackend($uid, $password, UserInterface $backend) {
427
+        $l = \OCP\Util::getL10N('lib');
428
+
429
+        $this->validateUserId($uid, true);
430
+
431
+        // No empty password
432
+        if (trim($password) === '') {
433
+            throw new \InvalidArgumentException($l->t('A valid password must be provided'));
434
+        }
435
+
436
+        // Check if user already exists
437
+        if ($this->userExists($uid)) {
438
+            throw new \InvalidArgumentException($l->t('The Login is already being used'));
439
+        }
440
+
441
+        /** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
442
+        $this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
443
+        $this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
444
+        $state = $backend->createUser($uid, $password);
445
+        if ($state === false) {
446
+            throw new \InvalidArgumentException($l->t('Could not create account'));
447
+        }
448
+        $user = $this->getUserObject($uid, $backend);
449
+        if ($user instanceof IUser) {
450
+            /** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
451
+            $this->emit('\OC\User', 'postCreateUser', [$user, $password]);
452
+            $this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
453
+            return $user;
454
+        }
455
+        return false;
456
+    }
457
+
458
+    /**
459
+     * returns how many users per backend exist (if supported by backend)
460
+     *
461
+     * @param boolean $hasLoggedIn when true only users that have a lastLogin
462
+     *                             entry in the preferences table will be affected
463
+     * @return array<string, int> an array of backend class as key and count number as value
464
+     */
465
+    public function countUsers() {
466
+        $userCountStatistics = [];
467
+        foreach ($this->backends as $backend) {
468
+            if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
469
+                /** @var ICountUsersBackend|IUserBackend $backend */
470
+                $backendUsers = $backend->countUsers();
471
+                if ($backendUsers !== false) {
472
+                    if ($backend instanceof IUserBackend) {
473
+                        $name = $backend->getBackendName();
474
+                    } else {
475
+                        $name = get_class($backend);
476
+                    }
477
+                    if (isset($userCountStatistics[$name])) {
478
+                        $userCountStatistics[$name] += $backendUsers;
479
+                    } else {
480
+                        $userCountStatistics[$name] = $backendUsers;
481
+                    }
482
+                }
483
+            }
484
+        }
485
+        return $userCountStatistics;
486
+    }
487
+
488
+    public function countUsersTotal(int $limit = 0, bool $onlyMappedUsers = false): int|false {
489
+        $userCount = false;
490
+
491
+        foreach ($this->backends as $backend) {
492
+            if ($onlyMappedUsers && $backend instanceof ICountMappedUsersBackend) {
493
+                $backendUsers = $backend->countMappedUsers();
494
+            } elseif ($backend instanceof ILimitAwareCountUsersBackend) {
495
+                $backendUsers = $backend->countUsers($limit);
496
+            } elseif ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
497
+                /** @var ICountUsersBackend $backend */
498
+                $backendUsers = $backend->countUsers();
499
+            } else {
500
+                $this->logger->debug('Skip backend for user count: ' . get_class($backend));
501
+                continue;
502
+            }
503
+            if ($backendUsers !== false) {
504
+                $userCount = (int)$userCount + $backendUsers;
505
+                if ($limit > 0) {
506
+                    if ($userCount >= $limit) {
507
+                        break;
508
+                    }
509
+                    $limit -= $userCount;
510
+                }
511
+            } else {
512
+                $this->logger->warning('Can not determine user count for ' . get_class($backend));
513
+            }
514
+        }
515
+        return $userCount;
516
+    }
517
+
518
+    /**
519
+     * returns how many users per backend exist in the requested groups (if supported by backend)
520
+     *
521
+     * @param IGroup[] $groups an array of groups to search in
522
+     * @param int $limit limit to stop counting
523
+     * @return array{int,int} total number of users, and number of disabled users in the given groups, below $limit. If limit is reached, -1 is returned for number of disabled users
524
+     */
525
+    public function countUsersAndDisabledUsersOfGroups(array $groups, int $limit): array {
526
+        $users = [];
527
+        $disabled = [];
528
+        foreach ($groups as $group) {
529
+            foreach ($group->getUsers() as $user) {
530
+                $users[$user->getUID()] = 1;
531
+                if (!$user->isEnabled()) {
532
+                    $disabled[$user->getUID()] = 1;
533
+                }
534
+                if (count($users) >= $limit) {
535
+                    return [count($users),-1];
536
+                }
537
+            }
538
+        }
539
+        return [count($users),count($disabled)];
540
+    }
541
+
542
+    /**
543
+     * The callback is executed for each user on each backend.
544
+     * If the callback returns false no further users will be retrieved.
545
+     *
546
+     * @psalm-param \Closure(\OCP\IUser):?bool $callback
547
+     * @param string $search
548
+     * @param boolean $onlySeen when true only users that have a lastLogin entry
549
+     *                          in the preferences table will be affected
550
+     * @since 9.0.0
551
+     */
552
+    public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
553
+        if ($onlySeen) {
554
+            $this->callForSeenUsers($callback);
555
+        } else {
556
+            foreach ($this->getBackends() as $backend) {
557
+                $limit = 500;
558
+                $offset = 0;
559
+                do {
560
+                    $users = $backend->getUsers($search, $limit, $offset);
561
+                    foreach ($users as $uid) {
562
+                        if (!$backend->userExists($uid)) {
563
+                            continue;
564
+                        }
565
+                        $user = $this->getUserObject($uid, $backend, false);
566
+                        $return = $callback($user);
567
+                        if ($return === false) {
568
+                            break;
569
+                        }
570
+                    }
571
+                    $offset += $limit;
572
+                } while (count($users) >= $limit);
573
+            }
574
+        }
575
+    }
576
+
577
+    /**
578
+     * returns how many users are disabled
579
+     *
580
+     * @return int
581
+     * @since 12.0.0
582
+     */
583
+    public function countDisabledUsers(): int {
584
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
585
+        $queryBuilder->select($queryBuilder->func()->count('*'))
586
+            ->from('preferences')
587
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
588
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
589
+            ->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));
590
+
591
+
592
+        $result = $queryBuilder->execute();
593
+        $count = $result->fetchOne();
594
+        $result->closeCursor();
595
+
596
+        if ($count !== false) {
597
+            $count = (int)$count;
598
+        } else {
599
+            $count = 0;
600
+        }
601
+
602
+        return $count;
603
+    }
604
+
605
+    /**
606
+     * returns how many users have logged in once
607
+     *
608
+     * @return int
609
+     * @since 11.0.0
610
+     */
611
+    public function countSeenUsers() {
612
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
613
+        $queryBuilder->select($queryBuilder->func()->count('*'))
614
+            ->from('preferences')
615
+            ->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
616
+            ->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')));
617
+
618
+        $query = $queryBuilder->execute();
619
+
620
+        $result = (int)$query->fetchOne();
621
+        $query->closeCursor();
622
+
623
+        return $result;
624
+    }
625
+
626
+    public function callForSeenUsers(\Closure $callback) {
627
+        $users = $this->getSeenUsers();
628
+        foreach ($users as $user) {
629
+            $return = $callback($user);
630
+            if ($return === false) {
631
+                return;
632
+            }
633
+        }
634
+    }
635
+
636
+    /**
637
+     * Getting all userIds that have a listLogin value requires checking the
638
+     * value in php because on oracle you cannot use a clob in a where clause,
639
+     * preventing us from doing a not null or length(value) > 0 check.
640
+     *
641
+     * @param int $limit
642
+     * @param int $offset
643
+     * @return string[] with user ids
644
+     */
645
+    private function getSeenUserIds($limit = null, $offset = null) {
646
+        $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
647
+        $queryBuilder->select(['userid'])
648
+            ->from('preferences')
649
+            ->where($queryBuilder->expr()->eq(
650
+                'appid', $queryBuilder->createNamedParameter('login'))
651
+            )
652
+            ->andWhere($queryBuilder->expr()->eq(
653
+                'configkey', $queryBuilder->createNamedParameter('lastLogin'))
654
+            )
655
+            ->andWhere($queryBuilder->expr()->isNotNull('configvalue')
656
+            );
657
+
658
+        if ($limit !== null) {
659
+            $queryBuilder->setMaxResults($limit);
660
+        }
661
+        if ($offset !== null) {
662
+            $queryBuilder->setFirstResult($offset);
663
+        }
664
+        $query = $queryBuilder->execute();
665
+        $result = [];
666
+
667
+        while ($row = $query->fetch()) {
668
+            $result[] = $row['userid'];
669
+        }
670
+
671
+        $query->closeCursor();
672
+
673
+        return $result;
674
+    }
675
+
676
+    /**
677
+     * @param string $email
678
+     * @return IUser[]
679
+     * @since 9.1.0
680
+     */
681
+    public function getByEmail($email) {
682
+        // looking for 'email' only (and not primary_mail) is intentional
683
+        $userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);
684
+
685
+        $users = array_map(function ($uid) {
686
+            return $this->get($uid);
687
+        }, $userIds);
688
+
689
+        return array_values(array_filter($users, function ($u) {
690
+            return ($u instanceof IUser);
691
+        }));
692
+    }
693
+
694
+    /**
695
+     * @param string $uid
696
+     * @param bool $checkDataDirectory
697
+     * @throws \InvalidArgumentException Message is an already translated string with a reason why the id is not valid
698
+     * @since 26.0.0
699
+     */
700
+    public function validateUserId(string $uid, bool $checkDataDirectory = false): void {
701
+        $l = Server::get(IFactory::class)->get('lib');
702
+
703
+        // Check the ID for bad characters
704
+        // Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'"
705
+        if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
706
+            throw new \InvalidArgumentException($l->t('Only the following characters are allowed in an Login:'
707
+                . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'));
708
+        }
709
+
710
+        // No empty user ID
711
+        if (trim($uid) === '') {
712
+            throw new \InvalidArgumentException($l->t('A valid Login must be provided'));
713
+        }
714
+
715
+        // No whitespace at the beginning or at the end
716
+        if (trim($uid) !== $uid) {
717
+            throw new \InvalidArgumentException($l->t('Login contains whitespace at the beginning or at the end'));
718
+        }
719
+
720
+        // User ID only consists of 1 or 2 dots (directory traversal)
721
+        if ($uid === '.' || $uid === '..') {
722
+            throw new \InvalidArgumentException($l->t('Login must not consist of dots only'));
723
+        }
724
+
725
+        // User ID is too long
726
+        if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
727
+            // TRANSLATORS User ID is too long
728
+            throw new \InvalidArgumentException($l->t('Username is too long'));
729
+        }
730
+
731
+        if (!$this->verifyUid($uid, $checkDataDirectory)) {
732
+            throw new \InvalidArgumentException($l->t('Login is invalid because files already exist for this user'));
733
+        }
734
+    }
735
+
736
+    /**
737
+     * Gets the list of user ids sorted by lastLogin, from most recent to least recent
738
+     *
739
+     * @param int|null $limit how many users to fetch (default: 25, max: 100)
740
+     * @param int $offset from which offset to fetch
741
+     * @param string $search search users based on search params
742
+     * @return list<string> list of user IDs
743
+     */
744
+    public function getLastLoggedInUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
745
+        // We can't load all users who already logged in
746
+        $limit = min(100, $limit ?: 25);
747
+
748
+        $connection = \OC::$server->getDatabaseConnection();
749
+        $queryBuilder = $connection->getQueryBuilder();
750
+        $queryBuilder->select('pref_login.userid')
751
+            ->from('preferences', 'pref_login')
752
+            ->where($queryBuilder->expr()->eq('pref_login.appid', $queryBuilder->expr()->literal('login')))
753
+            ->andWhere($queryBuilder->expr()->eq('pref_login.configkey', $queryBuilder->expr()->literal('lastLogin')))
754
+            ->setFirstResult($offset)
755
+            ->setMaxResults($limit)
756
+        ;
757
+
758
+        // Oracle don't want to run ORDER BY on CLOB column
759
+        $loginOrder = $connection->getDatabasePlatform() instanceof OraclePlatform
760
+            ? $queryBuilder->expr()->castColumn('pref_login.configvalue', IQueryBuilder::PARAM_INT)
761
+            : 'pref_login.configvalue';
762
+        $queryBuilder
763
+            ->orderBy($loginOrder, 'DESC')
764
+            ->addOrderBy($queryBuilder->func()->lower('pref_login.userid'), 'ASC');
765
+
766
+        if ($search !== '') {
767
+            $displayNameMatches = $this->searchDisplayName($search);
768
+            $matchedUids = array_map(static fn (IUser $u): string => $u->getUID(), $displayNameMatches);
769
+
770
+            $queryBuilder
771
+                ->leftJoin('pref_login', 'preferences', 'pref_email', $queryBuilder->expr()->andX(
772
+                    $queryBuilder->expr()->eq('pref_login.userid', 'pref_email.userid'),
773
+                    $queryBuilder->expr()->eq('pref_email.appid', $queryBuilder->expr()->literal('settings')),
774
+                    $queryBuilder->expr()->eq('pref_email.configkey', $queryBuilder->expr()->literal('email')),
775
+                ))
776
+                ->andWhere($queryBuilder->expr()->orX(
777
+                    $queryBuilder->expr()->in('pref_login.userid', $queryBuilder->createNamedParameter($matchedUids, IQueryBuilder::PARAM_STR_ARRAY)),
778
+                ));
779
+        }
780
+
781
+        /** @var list<string> */
782
+        $list = $queryBuilder->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
783
+
784
+        return $list;
785
+    }
786
+
787
+    private function verifyUid(string $uid, bool $checkDataDirectory = false): bool {
788
+        $appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');
789
+
790
+        if (\in_array($uid, [
791
+            '.htaccess',
792
+            'files_external',
793
+            '__groupfolders',
794
+            '.ncdata',
795
+            'owncloud.log',
796
+            'nextcloud.log',
797
+            'updater.log',
798
+            'audit.log',
799
+            $appdata], true)) {
800
+            return false;
801
+        }
802
+
803
+        if (!$checkDataDirectory) {
804
+            return true;
805
+        }
806
+
807
+        $dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');
808
+
809
+        return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
810
+    }
811
+
812
+    public function getDisplayNameCache(): DisplayNameCache {
813
+        return $this->displayNameCache;
814
+    }
815
+
816
+    /**
817
+     * Gets the list of users sorted by lastLogin, from most recent to least recent
818
+     *
819
+     * @param int $offset from which offset to fetch
820
+     * @return \Iterator<IUser> list of user IDs
821
+     * @since 30.0.0
822
+     */
823
+    public function getSeenUsers(int $offset = 0): \Iterator {
824
+        $limit = 1000;
825
+
826
+        do {
827
+            $userIds = $this->getSeenUserIds($limit, $offset);
828
+            $offset += $limit;
829
+
830
+            foreach ($userIds as $userId) {
831
+                foreach ($this->backends as $backend) {
832
+                    if ($backend->userExists($userId)) {
833
+                        $user = $this->getUserObject($userId, $backend, false);
834
+                        yield $user;
835
+                        break;
836
+                    }
837
+                }
838
+            }
839
+        } while (count($userIds) === $limit);
840
+    }
841 841
 }
Please login to merge, or discard this patch.