aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/User/Manager.php
blob: 152fb08eeeb9a7a98f4d342f57e8258271d8f896 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
generated by cgit v1.2.3 (git 2.39.1) at 2025-08-04 12:55:57 +0000
 


 href='#n770'>770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC\User;

use Doctrine\DBAL\Platforms\OraclePlatform;
use OC\Hooks\PublicEmitter;
use OC\Memcache\WithLocalCache;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\HintException;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IUser;
use OCP\IUserBackend;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Support\Subscription\IAssertion;
use OCP\User\Backend\ICheckPasswordBackend;
use OCP\User\Backend\ICountMappedUsersBackend;
use OCP\User\Backend\ICountUsersBackend;
use OCP\User\Backend\IGetRealUIDBackend;
use OCP\User\Backend\ILimitAwareCountUsersBackend;
use OCP\User\Backend\IProvideEnabledStateBackend;
use OCP\User\Backend\ISearchKnownUsersBackend;
use OCP\User\Events\BeforeUserCreatedEvent;
use OCP\User\Events\UserCreatedEvent;
use OCP\UserInterface;
use Psr\Log\LoggerInterface;

/**
 * Class Manager
 *
 * Hooks available in scope \OC\User:
 * - preSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
 * - postSetPassword(\OC\User\User $user, string $password, string $recoverPassword)
 * - preDelete(\OC\User\User $user)
 * - postDelete(\OC\User\User $user)
 * - preCreateUser(string $uid, string $password)
 * - postCreateUser(\OC\User\User $user, string $password)
 * - change(\OC\User\User $user)
 * - assignedUserId(string $uid)
 * - preUnassignedUserId(string $uid)
 * - postUnassignedUserId(string $uid)
 *
 * @package OC\User
 */
class Manager extends PublicEmitter implements IUserManager {
	/**
	 * @var \OCP\UserInterface[] $backends
	 */
	private array $backends = [];

	/**
	 * @var array<string,\OC\User\User> $cachedUsers
	 */
	private array $cachedUsers = [];

	private ICache $cache;

	private DisplayNameCache $displayNameCache;

	public function __construct(
		private IConfig $config,
		ICacheFactory $cacheFactory,
		private IEventDispatcher $eventDispatcher,
		private LoggerInterface $logger,
	) {
		$this->cache = new WithLocalCache($cacheFactory->createDistributed('user_backend_map'));
		$this->listen('\OC\User', 'postDelete', function (IUser $user): void {
			unset($this->cachedUsers[$user->getUID()]);
		});
		$this->displayNameCache = new DisplayNameCache($cacheFactory, $this);
	}

	/**
	 * Get the active backends
	 * @return \OCP\UserInterface[]
	 */
	public function getBackends() {
		return $this->backends;
	}

	/**
	 * register a user backend
	 *
	 * @param \OCP\UserInterface $backend
	 */
	public function registerBackend($backend) {
		$this->backends[] = $backend;
	}

	/**
	 * remove a user backend
	 *
	 * @param \OCP\UserInterface $backend
	 */
	public function removeBackend($backend) {
		$this->cachedUsers = [];
		if (($i = array_search($backend, $this->backends)) !== false) {
			unset($this->backends[$i]);
		}
	}

	/**
	 * remove all user backends
	 */
	public function clearBackends() {
		$this->cachedUsers = [];
		$this->backends = [];
	}

	/**
	 * get a user by user id
	 *
	 * @param string $uid
	 * @return \OC\User\User|null Either the user or null if the specified user does not exist
	 */
	public function get($uid) {
		if (is_null($uid) || $uid === '' || $uid === false) {
			return null;
		}
		if (isset($this->cachedUsers[$uid])) { //check the cache first to prevent having to loop over the backends
			return $this->cachedUsers[$uid];
		}

		$cachedBackend = $this->cache->get(sha1($uid));
		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
			// Cache has the info of the user backend already, so ask that one directly
			$backend = $this->backends[$cachedBackend];
			if ($backend->userExists($uid)) {
				return $this->getUserObject($uid, $backend);
			}
		}

		foreach ($this->backends as $i => $backend) {
			if ($i === $cachedBackend) {
				// Tried that one already
				continue;
			}

			if ($backend->userExists($uid)) {
				// Hash $uid to ensure that only valid characters are used for the cache key
				$this->cache->set(sha1($uid), $i, 300);
				return $this->getUserObject($uid, $backend);
			}
		}
		return null;
	}

	public function getDisplayName(string $uid): ?string {
		return $this->displayNameCache->getDisplayName($uid);
	}

	/**
	 * get or construct the user object
	 *
	 * @param string $uid
	 * @param \OCP\UserInterface $backend
	 * @param bool $cacheUser If false the newly created user object will not be cached
	 * @return \OC\User\User
	 */
	public function getUserObject($uid, $backend, $cacheUser = true) {
		if ($backend instanceof IGetRealUIDBackend) {
			$uid = $backend->getRealUID($uid);
		}

		if (isset($this->cachedUsers[$uid])) {
			return $this->cachedUsers[$uid];
		}

		$user = new User($uid, $backend, $this->eventDispatcher, $this, $this->config);
		if ($cacheUser) {
			$this->cachedUsers[$uid] = $user;
		}
		return $user;
	}

	/**
	 * check if a user exists
	 *
	 * @param string $uid
	 * @return bool
	 */
	public function userExists($uid) {
		$user = $this->get($uid);
		return ($user !== null);
	}

	/**
	 * Check if the password is valid for the user
	 *
	 * @param string $loginName
	 * @param string $password
	 * @return IUser|false the User object on success, false otherwise
	 */
	public function checkPassword($loginName, $password) {
		$result = $this->checkPasswordNoLogging($loginName, $password);

		if ($result === false) {
			$this->logger->warning('Login failed: \'' . $loginName . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
		}

		return $result;
	}

	/**
	 * Check if the password is valid for the user
	 *
	 * @internal
	 * @param string $loginName
	 * @param string $password
	 * @return IUser|false the User object on success, false otherwise
	 */
	public function checkPasswordNoLogging($loginName, $password) {
		$loginName = str_replace("\0", '', $loginName);
		$password = str_replace("\0", '', $password);

		$cachedBackend = $this->cache->get($loginName);
		if ($cachedBackend !== null && isset($this->backends[$cachedBackend])) {
			$backends = [$this->backends[$cachedBackend]];
		} else {
			$backends = $this->backends;
		}
		foreach ($backends as $backend) {
			if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
				/** @var ICheckPasswordBackend $backend */
				$uid = $backend->checkPassword($loginName, $password);
				if ($uid !== false) {
					return $this->getUserObject($uid, $backend);
				}
			}
		}

		// since http basic auth doesn't provide a standard way of handling non ascii password we allow password to be urlencoded
		// we only do this decoding after using the plain password fails to maintain compatibility with any password that happens
		// to contain urlencoded patterns by "accident".
		$password = urldecode($password);

		foreach ($backends as $backend) {
			if ($backend instanceof ICheckPasswordBackend || $backend->implementsActions(Backend::CHECK_PASSWORD)) {
				/** @var ICheckPasswordBackend|UserInterface $backend */
				$uid = $backend->checkPassword($loginName, $password);
				if ($uid !== false) {
					return $this->getUserObject($uid, $backend);
				}
			}
		}

		return false;
	}

	/**
	 * Search by user id
	 *
	 * @param string $pattern
	 * @param int $limit
	 * @param int $offset
	 * @return IUser[]
	 * @deprecated 27.0.0, use searchDisplayName instead
	 */
	public function search($pattern, $limit = null, $offset = null) {
		$users = [];
		foreach ($this->backends as $backend) {
			$backendUsers = $backend->getUsers($pattern, $limit, $offset);
			if (is_array($backendUsers)) {
				foreach ($backendUsers as $uid) {
					$users[$uid] = new LazyUser($uid, $this, null, $backend);
				}
			}
		}

		uasort($users, function (IUser $a, IUser $b) {
			return strcasecmp($a->getUID(), $b->getUID());
		});
		return $users;
	}

	/**
	 * Search by displayName
	 *
	 * @param string $pattern
	 * @param int $limit
	 * @param int $offset
	 * @return IUser[]
	 */
	public function searchDisplayName($pattern, $limit = null, $offset = null) {
		$users = [];
		foreach ($this->backends as $backend) {
			$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
			if (is_array($backendUsers)) {
				foreach ($backendUsers as $uid => $displayName) {
					$users[] = new LazyUser($uid, $this, $displayName, $backend);
				}
			}
		}

		usort($users, function (IUser $a, IUser $b) {
			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
		});
		return $users;
	}

	/**
	 * @return IUser[]
	 */
	public function getDisabledUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
		$users = $this->config->getUsersForUserValue('core', 'enabled', 'false');
		$users = array_combine(
			$users,
			array_map(
				fn (string $uid): IUser => new LazyUser($uid, $this),
				$users
			)
		);
		if ($search !== '') {
			$users = array_filter(
				$users,
				function (IUser $user) use ($search): bool {
					try {
						return mb_stripos($user->getUID(), $search) !== false ||
						mb_stripos($user->getDisplayName(), $search) !== false ||
						mb_stripos($user->getEMailAddress() ?? '', $search) !== false;
					} catch (NoUserException $ex) {
						$this->logger->error('Error while filtering disabled users', ['exception' => $ex, 'userUID' => $user->getUID()]);
						return false;
					}
				});
		}

		$tempLimit = ($limit === null ? null : $limit + $offset);
		foreach ($this->backends as $backend) {
			if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
				break;
			}
			if ($backend instanceof IProvideEnabledStateBackend) {
				$backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)), 0, $search);
				foreach ($backendUsers as $uid) {
					$users[$uid] = new LazyUser($uid, $this, null, $backend);
				}
			}
		}

		return array_slice($users, $offset, $limit);
	}

	/**
	 * Search known users (from phonebook sync) by displayName
	 *
	 * @param string $searcher
	 * @param string $pattern
	 * @param int|null $limit
	 * @param int|null $offset
	 * @return IUser[]
	 */
	public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
		$users = [];
		foreach ($this->backends as $backend) {
			if ($backend instanceof ISearchKnownUsersBackend) {
				$backendUsers = $backend->searchKnownUsersByDisplayName($searcher, $pattern, $limit, $offset);
			} else {
				// Better than nothing, but filtering after pagination can remove lots of results.
				$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
			}
			if (is_array($backendUsers)) {
				foreach ($backendUsers as $uid => $displayName) {
					$users[] = $this->getUserObject($uid, $backend);
				}
			}
		}

		usort($users, function ($a, $b) {
			/**
			 * @var IUser $a
			 * @var IUser $b
			 */
			return strcasecmp($a->getDisplayName(), $b->getDisplayName());
		});
		return $users;
	}

	/**
	 * @param string $uid
	 * @param string $password
	 * @return false|IUser the created user or false
	 * @throws \InvalidArgumentException
	 * @throws HintException
	 */
	public function createUser($uid, $password) {
		// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
		/** @var IAssertion $assertion */
		$assertion = \OC::$server->get(IAssertion::class);
		$assertion->createUserIsLegit();

		$localBackends = [];
		foreach ($this->backends as $backend) {
			if ($backend instanceof Database) {
				// First check if there is another user backend
				$localBackends[] = $backend;
				continue;
			}

			if ($backend->implementsActions(Backend::CREATE_USER)) {
				return $this->createUserFromBackend($uid, $password, $backend);
			}
		}

		foreach ($localBackends as $backend) {
			if ($backend->implementsActions(Backend::CREATE_USER)) {
				return $this->createUserFromBackend($uid, $password, $backend);
			}
		}

		return false;
	}

	/**
	 * @param string $uid
	 * @param string $password
	 * @param UserInterface $backend
	 * @return IUser|false
	 * @throws \InvalidArgumentException
	 */
	public function createUserFromBackend($uid, $password, UserInterface $backend) {
		$l = \OCP\Util::getL10N('lib');

		$this->validateUserId($uid, true);

		// No empty password
		if (trim($password) === '') {
			throw new \InvalidArgumentException($l->t('A valid password must be provided'));
		}

		// Check if user already exists
		if ($this->userExists($uid)) {
			throw new \InvalidArgumentException($l->t('The Login is already being used'));
		}

		/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
		$this->emit('\OC\User', 'preCreateUser', [$uid, $password]);
		$this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
		$state = $backend->createUser($uid, $password);
		if ($state === false) {
			throw new \InvalidArgumentException($l->t('Could not create account'));
		}
		$user = $this->getUserObject($uid, $backend);
		if ($user instanceof IUser) {
			/** @deprecated 21.0.0 use UserCreatedEvent event with the IEventDispatcher instead */
			$this->emit('\OC\User', 'postCreateUser', [$user, $password]);
			$this->eventDispatcher->dispatchTyped(new UserCreatedEvent($user, $password));
			return $user;
		}
		return false;
	}

	/**
	 * returns how many users per backend exist (if supported by backend)
	 *
	 * @param boolean $hasLoggedIn when true only users that have a lastLogin
	 *                             entry in the preferences table will be affected
	 * @return array<string, int> an array of backend class as key and count number as value
	 */
	public function countUsers() {
		$userCountStatistics = [];
		foreach ($this->backends as $backend) {
			if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
				/** @var ICountUsersBackend|IUserBackend $backend */
				$backendUsers = $backend->countUsers();
				if ($backendUsers !== false) {
					if ($backend instanceof IUserBackend) {
						$name = $backend->getBackendName();
					} else {
						$name = get_class($backend);
					}
					if (isset($userCountStatistics[$name])) {
						$userCountStatistics[$name] += $backendUsers;
					} else {
						$userCountStatistics[$name] = $backendUsers;
					}
				}
			}
		}
		return $userCountStatistics;
	}

	public function countUsersTotal(int $limit = 0, bool $onlyMappedUsers = false): int|false {
		$userCount = false;

		foreach ($this->backends as $backend) {
			if ($onlyMappedUsers && $backend instanceof ICountMappedUsersBackend) {
				$backendUsers = $backend->countMappedUsers();
			} elseif ($backend instanceof ILimitAwareCountUsersBackend) {
				$backendUsers = $backend->countUsers($limit);
			} elseif ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) {
				/** @var ICountUsersBackend $backend */
				$backendUsers = $backend->countUsers();
			} else {
				$this->logger->debug('Skip backend for user count: ' . get_class($backend));
				continue;
			}
			if ($backendUsers !== false) {
				$userCount = (int)$userCount + $backendUsers;
				if ($limit > 0) {
					if ($userCount >= $limit) {
						break;
					}
					$limit -= $userCount;
				}
			} else {
				$this->logger->warning('Can not determine user count for ' . get_class($backend));
			}
		}
		return $userCount;
	}

	/**
	 * returns how many users per backend exist in the requested groups (if supported by backend)
	 *
	 * @param IGroup[] $groups an array of groups to search in
	 * @param int $limit limit to stop counting
	 * @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
	 */
	public function countUsersAndDisabledUsersOfGroups(array $groups, int $limit): array {
		$users = [];
		$disabled = [];
		foreach ($groups as $group) {
			foreach ($group->getUsers() as $user) {
				$users[$user->getUID()] = 1;
				if (!$user->isEnabled()) {
					$disabled[$user->getUID()] = 1;
				}
				if (count($users) >= $limit) {
					return [count($users),-1];
				}
			}
		}
		return [count($users),count($disabled)];
	}

	/**
	 * The callback is executed for each user on each backend.
	 * If the callback returns false no further users will be retrieved.
	 *
	 * @psalm-param \Closure(\OCP\IUser):?bool $callback
	 * @param string $search
	 * @param boolean $onlySeen when true only users that have a lastLogin entry
	 *                          in the preferences table will be affected
	 * @since 9.0.0
	 */
	public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
		if ($onlySeen) {
			$this->callForSeenUsers($callback);
		} else {
			foreach ($this->getBackends() as $backend) {
				$limit = 500;
				$offset = 0;
				do {
					$users = $backend->getUsers($search, $limit, $offset);
					foreach ($users as $uid) {
						if (!$backend->userExists($uid)) {
							continue;
						}
						$user = $this->getUserObject($uid, $backend, false);
						$return = $callback($user);
						if ($return === false) {
							break;
						}
					}
					$offset += $limit;
				} while (count($users) >= $limit);
			}
		}
	}

	/**
	 * returns how many users are disabled
	 *
	 * @return int
	 * @since 12.0.0
	 */
	public function countDisabledUsers(): int {
		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
		$queryBuilder->select($queryBuilder->func()->count('*'))
			->from('preferences')
			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('core')))
			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('enabled')))
			->andWhere($queryBuilder->expr()->eq('configvalue', $queryBuilder->createNamedParameter('false'), IQueryBuilder::PARAM_STR));


		$result = $queryBuilder->execute();
		$count = $result->fetchOne();
		$result->closeCursor();

		if ($count !== false) {
			$count = (int)$count;
		} else {
			$count = 0;
		}

		return $count;
	}

	/**
	 * returns how many users have logged in once
	 *
	 * @return int
	 * @since 11.0.0
	 */
	public function countSeenUsers() {
		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
		$queryBuilder->select($queryBuilder->func()->count('*'))
			->from('preferences')
			->where($queryBuilder->expr()->eq('appid', $queryBuilder->createNamedParameter('login')))
			->andWhere($queryBuilder->expr()->eq('configkey', $queryBuilder->createNamedParameter('lastLogin')));

		$query = $queryBuilder->execute();

		$result = (int)$query->fetchOne();
		$query->closeCursor();

		return $result;
	}

	public function callForSeenUsers(\Closure $callback) {
		$users = $this->getSeenUsers();
		foreach ($users as $user) {
			$return = $callback($user);
			if ($return === false) {
				return;
			}
		}
	}

	/**
	 * Getting all userIds that have a listLogin value requires checking the
	 * value in php because on oracle you cannot use a clob in a where clause,
	 * preventing us from doing a not null or length(value) > 0 check.
	 *
	 * @param int $limit
	 * @param int $offset
	 * @return string[] with user ids
	 */
	private function getSeenUserIds($limit = null, $offset = null) {
		$queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
		$queryBuilder->select(['userid'])
			->from('preferences')
			->where($queryBuilder->expr()->eq(
				'appid', $queryBuilder->createNamedParameter('login'))
			)
			->andWhere($queryBuilder->expr()->eq(
				'configkey', $queryBuilder->createNamedParameter('lastLogin'))
			)
			->andWhere($queryBuilder->expr()->isNotNull('configvalue')
			);

		if ($limit !== null) {
			$queryBuilder->setMaxResults($limit);
		}
		if ($offset !== null) {
			$queryBuilder->setFirstResult($offset);
		}
		$query = $queryBuilder->execute();
		$result = [];

		while ($row = $query->fetch()) {
			$result[] = $row['userid'];
		}

		$query->closeCursor();

		return $result;
	}

	/**
	 * @param string $email
	 * @return IUser[]
	 * @since 9.1.0
	 */
	public function getByEmail($email) {
		// looking for 'email' only (and not primary_mail) is intentional
		$userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email);

		$users = array_map(function ($uid) {
			return $this->get($uid);
		}, $userIds);

		return array_values(array_filter($users, function ($u) {
			return ($u instanceof IUser);
		}));
	}

	/**
	 * @param string $uid
	 * @param bool $checkDataDirectory
	 * @throws \InvalidArgumentException Message is an already translated string with a reason why the id is not valid
	 * @since 26.0.0
	 */
	public function validateUserId(string $uid, bool $checkDataDirectory = false): void {
		$l = Server::get(IFactory::class)->get('lib');

		// Check the name for bad characters
		// Allowed are: "a-z", "A-Z", "0-9", spaces and "_.@-'"
		if (preg_match('/[^a-zA-Z0-9 _.@\-\']/', $uid)) {
			throw new \InvalidArgumentException($l->t('Only the following characters are allowed in an Login:'
				. ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'));
		}

		// No empty username
		if (trim($uid) === '') {
			throw new \InvalidArgumentException($l->t('A valid Login must be provided'));
		}

		// No whitespace at the beginning or at the end
		if (trim($uid) !== $uid) {
			throw new \InvalidArgumentException($l->t('Login contains whitespace at the beginning or at the end'));
		}

		// Username only consists of 1 or 2 dots (directory traversal)
		if ($uid === '.' || $uid === '..') {
			throw new \InvalidArgumentException($l->t('Login must not consist of dots only'));
		}

		if (!$this->verifyUid($uid, $checkDataDirectory)) {
			throw new \InvalidArgumentException($l->t('Login is invalid because files already exist for this user'));
		}
	}

	/**
	 * Gets the list of user ids sorted by lastLogin, from most recent to least recent
	 *
	 * @param int|null $limit how many users to fetch (default: 25, max: 100)
	 * @param int $offset from which offset to fetch
	 * @param string $search search users based on search params
	 * @return list<string> list of user IDs
	 */
	public function getLastLoggedInUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
		// We can't load all users who already logged in
		$limit = min(100, $limit ?: 25);

		$connection = \OC::$server->getDatabaseConnection();
		$queryBuilder = $connection->getQueryBuilder();
		$queryBuilder->select('pref_login.userid')
			->from('preferences', 'pref_login')
			->where($queryBuilder->expr()->eq('pref_login.appid', $queryBuilder->expr()->literal('login')))
			->andWhere($queryBuilder->expr()->eq('pref_login.configkey', $queryBuilder->expr()->literal('lastLogin')))
			->setFirstResult($offset)
			->setMaxResults($limit)
		;

		// Oracle don't want to run ORDER BY on CLOB column
		$loginOrder = $connection->getDatabasePlatform() instanceof OraclePlatform
			? $queryBuilder->expr()->castColumn('pref_login.configvalue', IQueryBuilder::PARAM_INT)
			: 'pref_login.configvalue';
		$queryBuilder
			->orderBy($loginOrder, 'DESC')
			->addOrderBy($queryBuilder->func()->lower('pref_login.userid'), 'ASC');

		if ($search !== '') {
			$displayNameMatches = $this->searchDisplayName($search);
			$matchedUids = array_map(static fn (IUser $u): string => $u->getUID(), $displayNameMatches);

			$queryBuilder
				->leftJoin('pref_login', 'preferences', 'pref_email', $queryBuilder->expr()->andX(
					$queryBuilder->expr()->eq('pref_login.userid', 'pref_email.userid'),
					$queryBuilder->expr()->eq('pref_email.appid', $queryBuilder->expr()->literal('settings')),
					$queryBuilder->expr()->eq('pref_email.configkey', $queryBuilder->expr()->literal('email')),
				))
				->andWhere($queryBuilder->expr()->orX(
					$queryBuilder->expr()->in('pref_login.userid', $queryBuilder->createNamedParameter($matchedUids, IQueryBuilder::PARAM_STR_ARRAY)),
				));
		}

		/** @var list<string> */
		$list = $queryBuilder->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);

		return $list;
	}

	private function verifyUid(string $uid, bool $checkDataDirectory = false): bool {
		$appdata = 'appdata_' . $this->config->getSystemValueString('instanceid');

		if (\in_array($uid, [
			'.htaccess',
			'files_external',
			'__groupfolders',
			'.ncdata',
			'owncloud.log',
			'nextcloud.log',
			'updater.log',
			'audit.log',
			$appdata], true)) {
			return false;
		}

		if (!$checkDataDirectory) {
			return true;
		}

		$dataDirectory = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data');

		return !file_exists(rtrim($dataDirectory, '/') . '/' . $uid);
	}

	public function getDisplayNameCache(): DisplayNameCache {
		return $this->displayNameCache;
	}

	/**
	 * Gets the list of users sorted by lastLogin, from most recent to least recent
	 *
	 * @param int $offset from which offset to fetch
	 * @return \Iterator<IUser> list of user IDs
	 * @since 30.0.0
	 */
	public function getSeenUsers(int $offset = 0): \Iterator {
		$limit = 1000;

		do {
			$userIds = $this->getSeenUserIds($limit, $offset);
			$offset += $limit;

			foreach ($userIds as $userId) {
				foreach ($this->backends as $backend) {
					if ($backend->userExists($userId)) {
						$user = $this->getUserObject($userId, $backend, false);
						yield $user;
						break;
					}
				}
			}
		} while (count($userIds) === $limit);
	}
}