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.php331
1 files changed, 192 insertions, 139 deletions
diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php
index 66a9529483e..097fd9a0dc8 100644
--- a/lib/private/User/Manager.php
+++ b/lib/private/User/Manager.php
@@ -1,38 +1,13 @@
<?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;
@@ -49,8 +24,10 @@ 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;
@@ -77,73 +54,52 @@ use Psr\Log\LoggerInterface;
*/
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 = [];
+ private array $cachedUsers = [];
- /** @var IConfig */
- private $config;
-
- /** @var ICache */
- private $cache;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
+ private ICache $cache;
private DisplayNameCache $displayNameCache;
- public function __construct(IConfig $config,
+ public function __construct(
+ private IConfig $config,
ICacheFactory $cacheFactory,
- IEventDispatcher $eventDispatcher) {
- $this->config = $config;
+ private IEventDispatcher $eventDispatcher,
+ private LoggerInterface $logger,
+ ) {
$this->cache = new WithLocalCache($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()]);
+ $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 = [];
}
@@ -162,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
@@ -221,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);
}
@@ -236,7 +200,7 @@ class Manager extends PublicEmitter implements IUserManager {
$result = $this->checkPasswordNoLogging($loginName, $password);
if ($result === false) {
- \OCP\Server::get(LoggerInterface::class)->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;
@@ -295,7 +259,7 @@ class Manager extends PublicEmitter implements IUserManager {
* @param int $limit
* @param int $offset
* @return IUser[]
- * @deprecated since 27.0.0, use searchDisplayName instead
+ * @deprecated 27.0.0, use searchDisplayName instead
*/
public function search($pattern, $limit = null, $offset = null) {
$users = [];
@@ -342,7 +306,7 @@ class Manager extends PublicEmitter implements IUserManager {
/**
* @return IUser[]
*/
- public function getDisabledUsers(?int $limit = null, int $offset = 0): array {
+ public function getDisabledUsers(?int $limit = null, int $offset = 0, string $search = ''): array {
$users = $this->config->getUsersForUserValue('core', 'enabled', 'false');
$users = array_combine(
$users,
@@ -351,6 +315,20 @@ class Manager extends PublicEmitter implements IUserManager {
$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) {
@@ -358,7 +336,7 @@ class Manager extends PublicEmitter implements IUserManager {
break;
}
if ($backend instanceof IProvideEnabledStateBackend) {
- $backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)));
+ $backendUsers = $backend->getDisabledUserList(($tempLimit === null ? null : $tempLimit - count($users)), 0, $search);
foreach ($backendUsers as $uid) {
$users[$uid] = new LazyUser($uid, $this, null, $backend);
}
@@ -481,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() {
@@ -507,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)];
}
/**
@@ -532,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) {
@@ -589,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
@@ -639,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);
+ }
}
/**
@@ -732,14 +700,14 @@ 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 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 Login must be provided'));
}
@@ -749,16 +717,73 @@ class Manager extends PublicEmitter implements IUserManager {
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('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('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');
@@ -766,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;
}
@@ -785,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);
+ }
}