aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/User/Manager.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/User/Manager.php')
-rw-r--r--lib/private/User/Manager.php416
1 files changed, 244 insertions, 172 deletions
diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php
index 937d825ed77..097fd9a0dc8 100644
--- a/lib/private/User/Manager.php
+++ b/lib/private/User/Manager.php
@@ -1,39 +1,15 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Chan <plus.vincchan@gmail.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * 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;
@@ -47,14 +23,17 @@ use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Server;
use OCP\Support\Subscription\IAssertion;
-use OCP\User\Backend\IGetRealUIDBackend;
-use OCP\User\Backend\ISearchKnownUsersBackend;
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 Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Psr\Log\LoggerInterface;
/**
* Class Manager
@@ -75,78 +54,52 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
*/
class Manager extends PublicEmitter implements IUserManager {
/**
- * @var \OCP\UserInterface[] $backends
+ * @var UserInterface[] $backends
*/
- private $backends = [];
+ private array $backends = [];
/**
- * @var \OC\User\User[] $cachedUsers
+ * @var array<string,\OC\User\User> $cachedUsers
*/
- private $cachedUsers = [];
-
- /** @var IConfig */
- private $config;
-
- /** @var EventDispatcherInterface */
- private $dispatcher;
-
- /** @var ICache */
- private $cache;
+ private array $cachedUsers = [];
- /** @var IEventDispatcher */
- private $eventDispatcher;
+ private ICache $cache;
private DisplayNameCache $displayNameCache;
- public function __construct(IConfig $config,
- EventDispatcherInterface $oldDispatcher,
- ICacheFactory $cacheFactory,
- IEventDispatcher $eventDispatcher) {
- $this->config = $config;
- $this->dispatcher = $oldDispatcher;
- $this->cache = $cacheFactory->createDistributed('user_backend_map');
- $cachedUsers = &$this->cachedUsers;
- $this->listen('\OC\User', 'postDelete', function ($user) use (&$cachedUsers) {
- /** @var \OC\User\User $user */
- unset($cachedUsers[$user->getUID()]);
+ 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->eventDispatcher = $eventDispatcher;
$this->displayNameCache = new DisplayNameCache($cacheFactory, $this);
}
/**
* Get the active backends
- * @return \OCP\UserInterface[]
+ * @return UserInterface[]
*/
- public function getBackends() {
+ public function getBackends(): array {
return $this->backends;
}
- /**
- * register a user backend
- *
- * @param \OCP\UserInterface $backend
- */
- public function registerBackend($backend) {
+ public function registerBackend(UserInterface $backend): void {
$this->backends[] = $backend;
}
- /**
- * remove a user backend
- *
- * @param \OCP\UserInterface $backend
- */
- public function removeBackend($backend) {
+ public function removeBackend(UserInterface $backend): void {
$this->cachedUsers = [];
if (($i = array_search($backend, $this->backends)) !== false) {
unset($this->backends[$i]);
}
}
- /**
- * remove all user backends
- */
- public function clearBackends() {
+ public function clearBackends(): void {
$this->cachedUsers = [];
$this->backends = [];
}
@@ -165,6 +118,10 @@ class Manager extends PublicEmitter implements IUserManager {
return $this->cachedUsers[$uid];
}
+ if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
+ return null;
+ }
+
$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
@@ -201,7 +158,7 @@ class Manager extends PublicEmitter implements IUserManager {
* @param bool $cacheUser If false the newly created user object will not be cached
* @return \OC\User\User
*/
- protected function getUserObject($uid, $backend, $cacheUser = true) {
+ public function getUserObject($uid, $backend, $cacheUser = true) {
if ($backend instanceof IGetRealUIDBackend) {
$uid = $backend->getRealUID($uid);
}
@@ -210,7 +167,7 @@ class Manager extends PublicEmitter implements IUserManager {
return $this->cachedUsers[$uid];
}
- $user = new User($uid, $backend, $this->dispatcher, $this, $this->config);
+ $user = new User($uid, $backend, $this->eventDispatcher, $this, $this->config);
if ($cacheUser) {
$this->cachedUsers[$uid] = $user;
}
@@ -224,6 +181,10 @@ class Manager extends PublicEmitter implements IUserManager {
* @return bool
*/
public function userExists($uid) {
+ if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
+ return false;
+ }
+
$user = $this->get($uid);
return ($user !== null);
}
@@ -239,7 +200,7 @@ class Manager extends PublicEmitter implements IUserManager {
$result = $this->checkPasswordNoLogging($loginName, $password);
if ($result === false) {
- \OC::$server->getLogger()->warning('Login failed: \''. $loginName .'\' (Remote IP: \''. \OC::$server->getRequest()->getRemoteAddress(). '\')', ['app' => 'core']);
+ $this->logger->warning('Login failed: \'' . $loginName . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
}
return $result;
@@ -292,12 +253,13 @@ class Manager extends PublicEmitter implements IUserManager {
}
/**
- * search by user id
+ * Search by user id
*
* @param string $pattern
* @param int $limit
* @param int $offset
- * @return \OC\User\User[]
+ * @return IUser[]
+ * @deprecated 27.0.0, use searchDisplayName instead
*/
public function search($pattern, $limit = null, $offset = null) {
$users = [];
@@ -305,28 +267,24 @@ class Manager extends PublicEmitter implements IUserManager {
$backendUsers = $backend->getUsers($pattern, $limit, $offset);
if (is_array($backendUsers)) {
foreach ($backendUsers as $uid) {
- $users[$uid] = $this->getUserObject($uid, $backend);
+ $users[$uid] = new LazyUser($uid, $this, null, $backend);
}
}
}
- uasort($users, function ($a, $b) {
- /**
- * @var \OC\User\User $a
- * @var \OC\User\User $b
- */
+ uasort($users, function (IUser $a, IUser $b) {
return strcasecmp($a->getUID(), $b->getUID());
});
return $users;
}
/**
- * search by displayName
+ * Search by displayName
*
* @param string $pattern
* @param int $limit
* @param int $offset
- * @return \OC\User\User[]
+ * @return IUser[]
*/
public function searchDisplayName($pattern, $limit = null, $offset = null) {
$users = [];
@@ -334,22 +292,61 @@ class Manager extends PublicEmitter implements IUserManager {
$backendUsers = $backend->getDisplayNames($pattern, $limit, $offset);
if (is_array($backendUsers)) {
foreach ($backendUsers as $uid => $displayName) {
- $users[] = $this->getUserObject($uid, $backend);
+ $users[] = new LazyUser($uid, $this, $displayName, $backend);
}
}
}
- usort($users, function ($a, $b) {
- /**
- * @var \OC\User\User $a
- * @var \OC\User\User $b
- */
+ 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
@@ -427,7 +424,7 @@ class Manager extends PublicEmitter implements IUserManager {
* @throws \InvalidArgumentException
*/
public function createUserFromBackend($uid, $password, UserInterface $backend) {
- $l = \OC::$server->getL10N('lib');
+ $l = \OCP\Util::getL10N('lib');
$this->validateUserId($uid, true);
@@ -438,7 +435,7 @@ class Manager extends PublicEmitter implements IUserManager {
// Check if user already exists
if ($this->userExists($uid)) {
- throw new \InvalidArgumentException($l->t('The username is already being used'));
+ throw new \InvalidArgumentException($l->t('The Login is already being used'));
}
/** @deprecated 21.0.0 use BeforeUserCreatedEvent event with the IEventDispatcher instead */
@@ -446,7 +443,7 @@ class Manager extends PublicEmitter implements IUserManager {
$this->eventDispatcher->dispatchTyped(new BeforeUserCreatedEvent($uid, $password));
$state = $backend->createUser($uid, $password);
if ($state === false) {
- throw new \InvalidArgumentException($l->t('Could not create user'));
+ throw new \InvalidArgumentException($l->t('Could not create account'));
}
$user = $this->getUserObject($uid, $backend);
if ($user instanceof IUser) {
@@ -462,7 +459,7 @@ class Manager extends PublicEmitter implements IUserManager {
* 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
+ * 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() {
@@ -488,22 +485,58 @@ class Manager extends PublicEmitter implements IUserManager {
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 gid to search in
- * @return array|int an array of backend class as key and count number as value
- * if $hasLoggedIn is true only an int is returned
+ * @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 countUsersOfGroups(array $groups) {
+ public function countUsersAndDisabledUsersOfGroups(array $groups, int $limit): array {
$users = [];
+ $disabled = [];
foreach ($groups as $group) {
- $usersIds = array_map(function ($user) {
- return $user->getUID();
- }, $group->getUsers());
- $users = array_merge($users, $usersIds);
+ 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(array_unique($users));
+ return [count($users),count($disabled)];
}
/**
@@ -513,7 +546,7 @@ class Manager extends PublicEmitter implements IUserManager {
* @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
+ * in the preferences table will be affected
* @since 9.0.0
*/
public function callForAllUsers(\Closure $callback, $search = '', $onlySeen = false) {
@@ -570,36 +603,6 @@ class Manager extends PublicEmitter implements IUserManager {
}
/**
- * returns how many users are disabled in the requested groups
- *
- * @param array $groups groupids to search
- * @return int
- * @since 14.0.0
- */
- public function countDisabledUsersOfGroups(array $groups): int {
- $queryBuilder = \OC::$server->getDatabaseConnection()->getQueryBuilder();
- $queryBuilder->select($queryBuilder->createFunction('COUNT(DISTINCT ' . $queryBuilder->getColumnName('uid') . ')'))
- ->from('preferences', 'p')
- ->innerJoin('p', 'group_user', 'g', $queryBuilder->expr()->eq('p.userid', 'g.uid'))
- ->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))
- ->andWhere($queryBuilder->expr()->in('gid', $queryBuilder->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)));
-
- $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
@@ -620,30 +623,14 @@ class Manager extends PublicEmitter implements IUserManager {
return $result;
}
- /**
- * @param \Closure $callback
- * @psalm-param \Closure(\OCP\IUser):?bool $callback
- * @since 11.0.0
- */
public function callForSeenUsers(\Closure $callback) {
- $limit = 1000;
- $offset = 0;
- 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);
- $return = $callback($user);
- if ($return === false) {
- return;
- }
- break;
- }
- }
+ $users = $this->getSeenUsers();
+ foreach ($users as $user) {
+ $return = $callback($user);
+ if ($return === false) {
+ return;
}
- } while (count($userIds) >= $limit);
+ }
}
/**
@@ -713,33 +700,90 @@ class Manager extends PublicEmitter implements IUserManager {
public function validateUserId(string $uid, bool $checkDataDirectory = false): void {
$l = Server::get(IFactory::class)->get('lib');
- // Check the name for bad characters
+ // Check the ID 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 a username:'
+ 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
+ // No empty user ID
if (trim($uid) === '') {
- throw new \InvalidArgumentException($l->t('A valid username must be provided'));
+ 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('Username contains whitespace at the beginning or at the end'));
+ 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)
+ // User ID only consists of 1 or 2 dots (directory traversal)
if ($uid === '.' || $uid === '..') {
- throw new \InvalidArgumentException($l->t('Username must not consist of dots only'));
+ throw new \InvalidArgumentException($l->t('Login must not consist of dots only'));
+ }
+
+ // User ID is too long
+ if (strlen($uid) > IUser::MAX_USERID_LENGTH) {
+ // TRANSLATORS User ID is too long
+ throw new \InvalidArgumentException($l->t('Username is too long'));
}
if (!$this->verifyUid($uid, $checkDataDirectory)) {
- throw new \InvalidArgumentException($l->t('Username is invalid because files already exist for this user'));
+ 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');
@@ -747,9 +791,11 @@ class Manager extends PublicEmitter implements IUserManager {
'.htaccess',
'files_external',
'__groupfolders',
- '.ocdata',
+ '.ncdata',
'owncloud.log',
'nextcloud.log',
+ 'updater.log',
+ 'audit.log',
$appdata], true)) {
return false;
}
@@ -766,4 +812,30 @@ class Manager extends PublicEmitter implements IUserManager {
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);
+ }
}