aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/User
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/User')
-rw-r--r--lib/private/User/Database.php278
-rw-r--r--lib/private/User/DisabledUserException.php10
-rw-r--r--lib/private/User/DisplayNameCache.php12
-rw-r--r--lib/private/User/LazyUser.php16
-rw-r--r--lib/private/User/Listeners/UserChangedListener.php2
-rw-r--r--lib/private/User/Manager.php191
-rw-r--r--lib/private/User/Session.php30
-rw-r--r--lib/private/User/User.php70
8 files changed, 359 insertions, 250 deletions
diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php
index d2ec835a25d..31488247939 100644
--- a/lib/private/User/Database.php
+++ b/lib/private/User/Database.php
@@ -12,16 +12,18 @@ use InvalidArgumentException;
use OCP\AppFramework\Db\TTransactional;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
use OCP\IDBConnection;
+use OCP\IUserManager;
use OCP\Security\Events\ValidatePasswordPolicyEvent;
use OCP\Security\IHasher;
use OCP\User\Backend\ABackend;
use OCP\User\Backend\ICheckPasswordBackend;
-use OCP\User\Backend\ICountUsersBackend;
use OCP\User\Backend\ICreateUserBackend;
use OCP\User\Backend\IGetDisplayNameBackend;
use OCP\User\Backend\IGetHomeBackend;
use OCP\User\Backend\IGetRealUIDBackend;
+use OCP\User\Backend\ILimitAwareCountUsersBackend;
use OCP\User\Backend\IPasswordHashBackend;
use OCP\User\Backend\ISearchKnownUsersBackend;
use OCP\User\Backend\ISetDisplayNameBackend;
@@ -37,21 +39,16 @@ class Database extends ABackend implements
IGetDisplayNameBackend,
ICheckPasswordBackend,
IGetHomeBackend,
- ICountUsersBackend,
+ ILimitAwareCountUsersBackend,
ISearchKnownUsersBackend,
IGetRealUIDBackend,
IPasswordHashBackend {
- /** @var CappedMemoryCache */
- private $cache;
- /** @var IEventDispatcher */
- private $eventDispatcher;
-
- /** @var IDBConnection */
- private $dbConn;
-
- /** @var string */
- private $table;
+ private CappedMemoryCache $cache;
+ private IConfig $config;
+ private ?IDBConnection $dbConnection;
+ private IEventDispatcher $eventDispatcher;
+ private string $table;
use TTransactional;
@@ -65,15 +62,18 @@ class Database extends ABackend implements
$this->cache = new CappedMemoryCache();
$this->table = $table;
$this->eventDispatcher = $eventDispatcher ?? \OCP\Server::get(IEventDispatcher::class);
+ $this->config = \OCP\Server::get(IConfig::class);
+ $this->dbConnection = null;
}
/**
* FIXME: This function should not be required!
*/
- private function fixDI() {
- if ($this->dbConn === null) {
- $this->dbConn = \OC::$server->getDatabaseConnection();
+ private function getDbConnection() {
+ if ($this->dbConnection === null) {
+ $this->dbConnection = \OCP\Server::get(IDBConnection::class);
}
+ return $this->dbConnection;
}
/**
@@ -87,52 +87,54 @@ class Database extends ABackend implements
* itself, not in its subclasses.
*/
public function createUser(string $uid, string $password): bool {
- $this->fixDI();
-
- if (!$this->userExists($uid)) {
- $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
-
- return $this->atomic(function () use ($uid, $password) {
- $qb = $this->dbConn->getQueryBuilder();
- $qb->insert($this->table)
- ->values([
- 'uid' => $qb->createNamedParameter($uid),
- 'password' => $qb->createNamedParameter(\OCP\Server::get(IHasher::class)->hash($password)),
- 'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)),
- ]);
-
- $result = $qb->executeStatement();
-
- // Clear cache
- unset($this->cache[$uid]);
- // Repopulate the cache
- $this->loadUser($uid);
-
- return (bool)$result;
- }, $this->dbConn);
+ if ($this->userExists($uid)) {
+ return false;
}
- return false;
+ $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
+
+ $dbConn = $this->getDbConnection();
+ return $this->atomic(function () use ($uid, $password, $dbConn) {
+ $qb = $dbConn->getQueryBuilder();
+ $qb->insert($this->table)
+ ->values([
+ 'uid' => $qb->createNamedParameter($uid),
+ 'password' => $qb->createNamedParameter(\OCP\Server::get(IHasher::class)->hash($password)),
+ 'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)),
+ ]);
+
+ $result = $qb->executeStatement();
+
+ // Clear cache
+ unset($this->cache[$uid]);
+ // Repopulate the cache
+ $this->loadUser($uid);
+
+ return (bool)$result;
+ }, $dbConn);
}
/**
- * delete a user
+ * Deletes a user
*
* @param string $uid The username of the user to delete
* @return bool
- *
- * Deletes a user
*/
public function deleteUser($uid) {
- $this->fixDI();
-
// Delete user-group-relation
- $query = $this->dbConn->getQueryBuilder();
+ $dbConn = $this->getDbConnection();
+ $query = $dbConn->getQueryBuilder();
$query->delete($this->table)
->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
$result = $query->executeStatement();
if (isset($this->cache[$uid])) {
+ // If the user logged in through email there is a second cache entry, also unset that.
+ $email = $this->cache[$uid]['email'] ?? null;
+ if ($email !== null) {
+ unset($this->cache[$email]);
+ }
+ // Unset the cache entry
unset($this->cache[$uid]);
}
@@ -140,7 +142,8 @@ class Database extends ABackend implements
}
private function updatePassword(string $uid, string $passwordHash): bool {
- $query = $this->dbConn->getQueryBuilder();
+ $dbConn = $this->getDbConnection();
+ $query = $dbConn->getQueryBuilder();
$query->update($this->table)
->set('password', $query->createNamedParameter($passwordHash))
->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
@@ -159,35 +162,34 @@ class Database extends ABackend implements
* Change the password of a user
*/
public function setPassword(string $uid, string $password): bool {
- $this->fixDI();
-
- if ($this->userExists($uid)) {
- $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
+ if (!$this->userExists($uid)) {
+ return false;
+ }
- $hasher = \OCP\Server::get(IHasher::class);
- $hashedPassword = $hasher->hash($password);
+ $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
- $return = $this->updatePassword($uid, $hashedPassword);
+ $hasher = \OCP\Server::get(IHasher::class);
+ $hashedPassword = $hasher->hash($password);
- if ($return) {
- $this->cache[$uid]['password'] = $hashedPassword;
- }
+ $return = $this->updatePassword($uid, $hashedPassword);
- return $return;
+ if ($return) {
+ $this->cache[$uid]['password'] = $hashedPassword;
}
- return false;
+ return $return;
}
public function getPasswordHash(string $userId): ?string {
- $this->fixDI();
if (!$this->userExists($userId)) {
return null;
}
if (!empty($this->cache[$userId]['password'])) {
return $this->cache[$userId]['password'];
}
- $qb = $this->dbConn->getQueryBuilder();
+
+ $dbConn = $this->getDbConnection();
+ $qb = $dbConn->getQueryBuilder();
$qb->select('password')
->from($this->table)
->where($qb->expr()->eq('uid_lower', $qb->createNamedParameter(mb_strtolower($userId))));
@@ -196,6 +198,7 @@ class Database extends ABackend implements
if ($hash === false) {
return null;
}
+
$this->cache[$userId]['password'] = $hash;
return $hash;
}
@@ -204,11 +207,12 @@ class Database extends ABackend implements
if (!\OCP\Server::get(IHasher::class)->validate($passwordHash)) {
throw new InvalidArgumentException();
}
- $this->fixDI();
+
$result = $this->updatePassword($userId, $passwordHash);
if (!$result) {
return false;
}
+
$this->cache[$userId]['password'] = $passwordHash;
return true;
}
@@ -229,21 +233,20 @@ class Database extends ABackend implements
throw new \InvalidArgumentException('Invalid displayname');
}
- $this->fixDI();
-
- if ($this->userExists($uid)) {
- $query = $this->dbConn->getQueryBuilder();
- $query->update($this->table)
- ->set('displayname', $query->createNamedParameter($displayName))
- ->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
- $query->executeStatement();
+ if (!$this->userExists($uid)) {
+ return false;
+ }
- $this->cache[$uid]['displayname'] = $displayName;
+ $dbConn = $this->getDbConnection();
+ $query = $dbConn->getQueryBuilder();
+ $query->update($this->table)
+ ->set('displayname', $query->createNamedParameter($displayName))
+ ->where($query->expr()->eq('uid_lower', $query->createNamedParameter(mb_strtolower($uid))));
+ $query->executeStatement();
- return true;
- }
+ $this->cache[$uid]['displayname'] = $displayName;
- return false;
+ return true;
}
/**
@@ -269,9 +272,8 @@ class Database extends ABackend implements
public function getDisplayNames($search = '', $limit = null, $offset = null) {
$limit = $this->fixLimit($limit);
- $this->fixDI();
-
- $query = $this->dbConn->getQueryBuilder();
+ $dbConn = $this->getDbConnection();
+ $query = $dbConn->getQueryBuilder();
$query->select('uid', 'displayname')
->from($this->table, 'u')
@@ -281,9 +283,9 @@ class Database extends ABackend implements
$query->expr()->eq('configkey', $query->expr()->literal('email')))
)
// sqlite doesn't like re-using a single named parameter here
- ->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
- ->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
- ->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $this->dbConn->escapeLikeParameter($search) . '%')))
+ ->where($query->expr()->iLike('uid', $query->createPositionalParameter('%' . $dbConn->escapeLikeParameter($search) . '%')))
+ ->orWhere($query->expr()->iLike('displayname', $query->createPositionalParameter('%' . $dbConn->escapeLikeParameter($search) . '%')))
+ ->orWhere($query->expr()->iLike('configvalue', $query->createPositionalParameter('%' . $dbConn->escapeLikeParameter($search) . '%')))
->orderBy($query->func()->lower('displayname'), 'ASC')
->addOrderBy('uid_lower', 'ASC')
->setMaxResults($limit)
@@ -309,9 +311,8 @@ class Database extends ABackend implements
public function searchKnownUsersByDisplayName(string $searcher, string $pattern, ?int $limit = null, ?int $offset = null): array {
$limit = $this->fixLimit($limit);
- $this->fixDI();
-
- $query = $this->dbConn->getQueryBuilder();
+ $dbConn = $this->getDbConnection();
+ $query = $dbConn->getQueryBuilder();
$query->select('u.uid', 'u.displayname')
->from($this->table, 'u')
@@ -321,8 +322,8 @@ class Database extends ABackend implements
))
->where($query->expr()->eq('k.known_to', $query->createNamedParameter($searcher)))
->andWhere($query->expr()->orX(
- $query->expr()->iLike('u.uid', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%')),
- $query->expr()->iLike('u.displayname', $query->createNamedParameter('%' . $this->dbConn->escapeLikeParameter($pattern) . '%'))
+ $query->expr()->iLike('u.uid', $query->createNamedParameter('%' . $dbConn->escapeLikeParameter($pattern) . '%')),
+ $query->expr()->iLike('u.displayname', $query->createNamedParameter('%' . $dbConn->escapeLikeParameter($pattern) . '%'))
))
->orderBy('u.displayname', 'ASC')
->addOrderBy('u.uid_lower', 'ASC')
@@ -341,7 +342,7 @@ class Database extends ABackend implements
/**
* Check if the password is correct
*
- * @param string $loginName The loginname
+ * @param string $loginName The login name
* @param string $password The password
* @return string
*
@@ -368,46 +369,64 @@ class Database extends ABackend implements
/**
* Load an user in the cache
*
- * @param string $uid the username
+ * @param string $loginName the username or email
* @return boolean true if user was found, false otherwise
*/
- private function loadUser($uid) {
- $this->fixDI();
+ private function loadUser(string $loginName, bool $tryEmail = true): bool {
+ if (isset($this->cache[$loginName])) {
+ return $this->cache[$loginName] !== false;
+ }
- $uid = (string)$uid;
- if (!isset($this->cache[$uid])) {
- //guests $uid could be NULL or ''
- if ($uid === '') {
- $this->cache[$uid] = false;
- return true;
- }
+ //guests $uid could be NULL or ''
+ if ($loginName === '') {
+ $this->cache[$loginName] = false;
+ return false;
+ }
+
+ $dbConn = $this->getDbConnection();
+ $qb = $dbConn->getQueryBuilder();
+ $qb->select('uid', 'displayname', 'password')
+ ->from($this->table)
+ ->where(
+ $qb->expr()->eq(
+ 'uid_lower', $qb->createNamedParameter(mb_strtolower($loginName))
+ )
+ );
+ $result = $qb->executeQuery();
+ $row = $result->fetch();
+ $result->closeCursor();
+
+ // "uid" is primary key, so there can only be a single result
+ if ($row !== false) {
+ $this->cache[$loginName] = [
+ 'uid' => (string)$row['uid'],
+ 'displayname' => (string)$row['displayname'],
+ 'password' => (string)$row['password'],
+ ];
+ return true;
+ }
- $qb = $this->dbConn->getQueryBuilder();
- $qb->select('uid', 'displayname', 'password')
- ->from($this->table)
- ->where(
- $qb->expr()->eq(
- 'uid_lower', $qb->createNamedParameter(mb_strtolower($uid))
- )
- );
- $result = $qb->executeQuery();
- $row = $result->fetch();
- $result->closeCursor();
-
- // "uid" is primary key, so there can only be a single result
- if ($row !== false) {
- $this->cache[$uid] = [
- 'uid' => (string)$row['uid'],
- 'displayname' => (string)$row['displayname'],
- 'password' => (string)$row['password'],
- ];
- } else {
- $this->cache[$uid] = false;
- return false;
+ // Not found by UID so we try also for email, load uid for email.
+ if ($tryEmail) {
+ /** @var string|null $uid Psalm does not get the type correct here */
+ [$uid] = [...$this->config->getUsersForUserValue('settings', 'email', mb_strtolower($loginName)), null];
+
+ // If found, try loading it
+ if ($uid !== null && $uid !== $loginName) {
+ $result = $this->loadUser($uid, false);
+ if ($result) {
+ // Also add cache result for the email
+ $this->cache[$loginName] = $this->cache[$uid];
+ // Set a reference to the uid cache entry for also delete email entry on user delete
+ $this->cache[$uid]['email'] = $loginName;
+ return true;
+ }
}
}
- return true;
+ // Not found by uid nor email, so cache as not existing
+ $this->cache[$loginName] = false;
+ return false;
}
/**
@@ -436,8 +455,7 @@ class Database extends ABackend implements
* @return boolean
*/
public function userExists($uid) {
- $this->loadUser($uid);
- return $this->cache[$uid] !== false;
+ return $this->loadUser($uid);
}
/**
@@ -448,7 +466,7 @@ class Database extends ABackend implements
*/
public function getHome(string $uid) {
if ($this->userExists($uid)) {
- return \OC::$server->getConfig()->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $uid;
+ return $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $uid;
}
return false;
@@ -463,18 +481,18 @@ class Database extends ABackend implements
/**
* counts the users in the database
- *
- * @return int|false
*/
- public function countUsers() {
- $this->fixDI();
-
- $query = $this->dbConn->getQueryBuilder();
+ public function countUsers(int $limit = 0): int|false {
+ $dbConn = $this->getDbConnection();
+ $query = $dbConn->getQueryBuilder();
$query->select($query->func()->count('uid'))
->from($this->table);
- $result = $query->executeQuery();
+ $result = $query->executeQuery()->fetchOne();
+ if ($result === false) {
+ return false;
+ }
- return $result->fetchOne();
+ return (int)$result;
}
/**
@@ -505,7 +523,7 @@ class Database extends ABackend implements
throw new \Exception('key uid is expected to be set in $param');
}
- $backends = \OC::$server->getUserManager()->getBackends();
+ $backends = \OCP\Server::get(IUserManager::class)->getBackends();
foreach ($backends as $backend) {
if ($backend instanceof Database) {
/** @var \OC\User\Database $backend */
diff --git a/lib/private/User/DisabledUserException.php b/lib/private/User/DisabledUserException.php
new file mode 100644
index 00000000000..db8a23d2027
--- /dev/null
+++ b/lib/private/User/DisabledUserException.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\User;
+
+class DisabledUserException extends LoginException {
+}
diff --git a/lib/private/User/DisplayNameCache.php b/lib/private/User/DisplayNameCache.php
index 34db783de68..4321d95f88e 100644
--- a/lib/private/User/DisplayNameCache.php
+++ b/lib/private/User/DisplayNameCache.php
@@ -11,6 +11,7 @@ use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\ICache;
use OCP\ICacheFactory;
+use OCP\IUser;
use OCP\IUserManager;
use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserDeletedEvent;
@@ -24,6 +25,8 @@ use OCP\User\Events\UserDeletedEvent;
* @template-implements IEventListener<UserChangedEvent|UserDeletedEvent>
*/
class DisplayNameCache implements IEventListener {
+ private const CACHE_TTL = 24 * 60 * 60; // 1 day
+
private array $cache = [];
private ICache $memCache;
private IUserManager $userManager;
@@ -37,6 +40,11 @@ class DisplayNameCache implements IEventListener {
if (isset($this->cache[$userId])) {
return $this->cache[$userId];
}
+
+ if (strlen($userId) > IUser::MAX_USERID_LENGTH) {
+ return null;
+ }
+
$displayName = $this->memCache->get($userId);
if ($displayName) {
$this->cache[$userId] = $displayName;
@@ -50,7 +58,7 @@ class DisplayNameCache implements IEventListener {
$displayName = null;
}
$this->cache[$userId] = $displayName;
- $this->memCache->set($userId, $displayName, 60 * 10); // 10 minutes
+ $this->memCache->set($userId, $displayName, self::CACHE_TTL);
return $displayName;
}
@@ -65,7 +73,7 @@ class DisplayNameCache implements IEventListener {
$userId = $event->getUser()->getUID();
$newDisplayName = $event->getValue();
$this->cache[$userId] = $newDisplayName;
- $this->memCache->set($userId, $newDisplayName, 60 * 10); // 10 minutes
+ $this->memCache->set($userId, $newDisplayName, self::CACHE_TTL);
}
if ($event instanceof UserDeletedEvent) {
$userId = $event->getUser()->getUID();
diff --git a/lib/private/User/LazyUser.php b/lib/private/User/LazyUser.php
index 92a0c735215..501169019d4 100644
--- a/lib/private/User/LazyUser.php
+++ b/lib/private/User/LazyUser.php
@@ -60,11 +60,15 @@ class LazyUser implements IUser {
return $this->getUser()->setDisplayName($displayName);
}
- public function getLastLogin() {
+ public function getLastLogin(): int {
return $this->getUser()->getLastLogin();
}
- public function updateLastLoginTimestamp() {
+ public function getFirstLogin(): int {
+ return $this->getUser()->getFirstLogin();
+ }
+
+ public function updateLastLoginTimestamp(): bool {
return $this->getUser()->updateLastLoginTimestamp();
}
@@ -108,6 +112,10 @@ class LazyUser implements IUser {
return $this->getUser()->canChangeDisplayName();
}
+ public function canChangeEmail(): bool {
+ return $this->getUser()->canChangeEmail();
+ }
+
public function isEnabled() {
return $this->getUser()->isEnabled();
}
@@ -152,6 +160,10 @@ class LazyUser implements IUser {
return $this->getUser()->getQuota();
}
+ public function getQuotaBytes(): int|float {
+ return $this->getUser()->getQuotaBytes();
+ }
+
public function setQuota($quota) {
$this->getUser()->setQuota($quota);
}
diff --git a/lib/private/User/Listeners/UserChangedListener.php b/lib/private/User/Listeners/UserChangedListener.php
index 983a4e81233..8f618950255 100644
--- a/lib/private/User/Listeners/UserChangedListener.php
+++ b/lib/private/User/Listeners/UserChangedListener.php
@@ -28,7 +28,7 @@ class UserChangedListener implements IEventListener {
if (!($event instanceof UserChangedEvent)) {
return;
}
-
+
$user = $event->getUser();
$feature = $event->getFeature();
$oldValue = $event->getOldValue();
diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php
index 7f5b06cc5f6..097fd9a0dc8 100644
--- a/lib/private/User/Manager.php
+++ b/lib/private/User/Manager.php
@@ -24,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;
@@ -52,7 +54,7 @@ use Psr\Log\LoggerInterface;
*/
class Manager extends PublicEmitter implements IUserManager {
/**
- * @var \OCP\UserInterface[] $backends
+ * @var UserInterface[] $backends
*/
private array $backends = [];
@@ -80,37 +82,24 @@ class Manager extends PublicEmitter implements IUserManager {
/**
* 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 = [];
}
@@ -129,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
@@ -188,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);
}
@@ -323,9 +320,9 @@ class Manager extends PublicEmitter implements IUserManager {
$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;
+ 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;
@@ -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)];
}
/**
@@ -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,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'));
}
@@ -730,11 +717,17 @@ 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'));
}
@@ -819,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);
+ }
}
diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php
index 408ebffb390..e7bfcf56407 100644
--- a/lib/private/User/Session.php
+++ b/lib/private/User/Session.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@@ -319,7 +320,7 @@ class Session implements IUserSession, Emitter {
// disabled users can not log in
// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
$message = \OCP\Util::getL10N('lib')->t('Account disabled');
- throw new LoginException($message);
+ throw new DisabledUserException($message);
}
if ($regenerateSessionId) {
@@ -780,7 +781,7 @@ class Session implements IUserSession, Emitter {
* Check if login names match
*/
private function validateTokenLoginName(?string $loginName, IToken $token): bool {
- if ($token->getLoginName() !== $loginName) {
+ if (mb_strtolower($token->getLoginName()) !== mb_strtolower($loginName ?? '')) {
// TODO: this makes it impossible to use different login names on browser and client
// e.g. login by e-mail 'user@example.com' on browser for generating the token will not
// allow to use the client token with the login name 'user'.
@@ -834,9 +835,8 @@ class Session implements IUserSession, Emitter {
return true;
}
- // Remember me tokens are not app_passwords
- if ($dbToken->getRemember() === IToken::DO_NOT_REMEMBER) {
- // Set the session variable so we know this is an app password
+ // Set the session variable so we know this is an app password
+ if ($dbToken instanceof PublicKeyToken && $dbToken->getType() === IToken::PERMANENT_TOKEN) {
$this->session->set('app_password', $token);
}
@@ -968,6 +968,7 @@ class Session implements IUserSession, Emitter {
if ($webRoot === '') {
$webRoot = '/';
}
+ $domain = $this->config->getSystemValueString('cookie_domain');
$maxAge = $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
\OC\Http\CookieHelper::setCookie(
@@ -975,7 +976,7 @@ class Session implements IUserSession, Emitter {
$username,
$maxAge,
$webRoot,
- '',
+ $domain,
$secureCookie,
true,
\OC\Http\CookieHelper::SAMESITE_LAX
@@ -985,7 +986,7 @@ class Session implements IUserSession, Emitter {
$token,
$maxAge,
$webRoot,
- '',
+ $domain,
$secureCookie,
true,
\OC\Http\CookieHelper::SAMESITE_LAX
@@ -996,7 +997,7 @@ class Session implements IUserSession, Emitter {
$this->session->getId(),
$maxAge,
$webRoot,
- '',
+ $domain,
$secureCookie,
true,
\OC\Http\CookieHelper::SAMESITE_LAX
@@ -1012,18 +1013,19 @@ class Session implements IUserSession, Emitter {
public function unsetMagicInCookie() {
//TODO: DI for cookies and IRequest
$secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https';
+ $domain = $this->config->getSystemValueString('cookie_domain');
unset($_COOKIE['nc_username']); //TODO: DI
unset($_COOKIE['nc_token']);
unset($_COOKIE['nc_session_id']);
- setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
- setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
- setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true);
+ setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, $domain, $secureCookie, true);
+ setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, $domain, $secureCookie, true);
+ setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT, $domain, $secureCookie, true);
// old cookies might be stored under /webroot/ instead of /webroot
// and Firefox doesn't like it!
- setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
- setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
- setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true);
+ setcookie('nc_username', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', $domain, $secureCookie, true);
+ setcookie('nc_token', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', $domain, $secureCookie, true);
+ setcookie('nc_session_id', '', $this->timeFactory->getTime() - 3600, OC::$WEBROOT . '/', $domain, $secureCookie, true);
}
/**
diff --git a/lib/private/User/User.php b/lib/private/User/User.php
index 4b1ec4366d0..1f908918b20 100644
--- a/lib/private/User/User.php
+++ b/lib/private/User/User.php
@@ -11,7 +11,6 @@ use InvalidArgumentException;
use OC\Accounts\AccountManager;
use OC\Avatar\AvatarManager;
use OC\Hooks\Emitter;
-use OC_Helper;
use OCP\Accounts\IAccountManager;
use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\IEventDispatcher;
@@ -65,8 +64,8 @@ class User implements IUser {
/** @var string */
private $home;
- /** @var int|null */
- private $lastLogin;
+ private ?int $lastLogin = null;
+ private ?int $firstLogin = null;
/** @var IAvatarManager */
private $avatarManager;
@@ -155,6 +154,7 @@ class User implements IUser {
*/
public function setSystemEMailAddress(string $mailAddress): void {
$oldMailAddress = $this->getSystemEMailAddress();
+ $mailAddress = mb_strtolower(trim($mailAddress));
if ($mailAddress === '') {
$this->config->deleteUserValue($this->uid, 'settings', 'email');
@@ -177,6 +177,7 @@ class User implements IUser {
* @inheritDoc
*/
public function setPrimaryEMailAddress(string $mailAddress): void {
+ $mailAddress = mb_strtolower(trim($mailAddress));
if ($mailAddress === '') {
$this->config->deleteUserValue($this->uid, 'settings', 'primary_email');
return;
@@ -202,28 +203,47 @@ class User implements IUser {
/**
* returns the timestamp of the user's last login or 0 if the user did never
* login
- *
- * @return int
*/
- public function getLastLogin() {
+ public function getLastLogin(): int {
if ($this->lastLogin === null) {
$this->lastLogin = (int)$this->config->getUserValue($this->uid, 'login', 'lastLogin', 0);
}
- return (int)$this->lastLogin;
+ return $this->lastLogin;
+ }
+
+ /**
+ * returns the timestamp of the user's last login or 0 if the user did never
+ * login
+ */
+ public function getFirstLogin(): int {
+ if ($this->firstLogin === null) {
+ $this->firstLogin = (int)$this->config->getUserValue($this->uid, 'login', 'firstLogin', 0);
+ }
+ return $this->firstLogin;
}
/**
* updates the timestamp of the most recent login of this user
*/
- public function updateLastLoginTimestamp() {
+ public function updateLastLoginTimestamp(): bool {
$previousLogin = $this->getLastLogin();
+ $firstLogin = $this->getFirstLogin();
$now = time();
$firstTimeLogin = $previousLogin === 0;
if ($now - $previousLogin > 60) {
- $this->lastLogin = time();
- $this->config->setUserValue(
- $this->uid, 'login', 'lastLogin', (string)$this->lastLogin);
+ $this->lastLogin = $now;
+ $this->config->setUserValue($this->uid, 'login', 'lastLogin', (string)$this->lastLogin);
+ }
+
+ if ($firstLogin === 0) {
+ if ($firstTimeLogin) {
+ $this->firstLogin = $now;
+ } else {
+ /* Unknown first login, most likely was before upgrade to Nextcloud 31 */
+ $this->firstLogin = -1;
+ }
+ $this->config->setUserValue($this->uid, 'login', 'firstLogin', (string)$this->firstLogin);
}
return $firstTimeLogin;
@@ -428,6 +448,11 @@ class User implements IUser {
return $this->backend->implementsActions(Backend::SET_DISPLAYNAME);
}
+ public function canChangeEmail(): bool {
+ // Fallback to display name value to avoid changing behavior with the new option.
+ return $this->config->getSystemValueBool('allow_user_to_change_email', $this->config->getSystemValueBool('allow_user_to_change_display_name', true));
+ }
+
/**
* check if the user is enabled
*
@@ -491,14 +516,16 @@ class User implements IUser {
* @inheritDoc
*/
public function getSystemEMailAddress(): ?string {
- return $this->config->getUserValue($this->uid, 'settings', 'email', null);
+ $email = $this->config->getUserValue($this->uid, 'settings', 'email', null);
+ return $email ? mb_strtolower(trim($email)) : null;
}
/**
* @inheritDoc
*/
public function getPrimaryEMailAddress(): ?string {
- return $this->config->getUserValue($this->uid, 'settings', 'primary_email', null);
+ $email = $this->config->getUserValue($this->uid, 'settings', 'primary_email', null);
+ return $email ? mb_strtolower(trim($email)) : null;
}
/**
@@ -535,6 +562,19 @@ class User implements IUser {
return $quota;
}
+ public function getQuotaBytes(): int|float {
+ $quota = $this->getQuota();
+ if ($quota === 'none') {
+ return \OCP\Files\FileInfo::SPACE_UNLIMITED;
+ }
+
+ $bytes = \OCP\Util::computerFileSize($quota);
+ if ($bytes === false) {
+ return \OCP\Files\FileInfo::SPACE_UNKNOWN;
+ }
+ return $bytes;
+ }
+
/**
* set the users' quota
*
@@ -546,11 +586,11 @@ class User implements IUser {
public function setQuota($quota) {
$oldQuota = $this->config->getUserValue($this->uid, 'files', 'quota', '');
if ($quota !== 'none' and $quota !== 'default') {
- $bytesQuota = OC_Helper::computerFileSize($quota);
+ $bytesQuota = \OCP\Util::computerFileSize($quota);
if ($bytesQuota === false) {
throw new InvalidArgumentException('Failed to set quota to invalid value ' . $quota);
}
- $quota = OC_Helper::humanFileSize($bytesQuota);
+ $quota = \OCP\Util::humanFileSize($bytesQuota);
}
if ($quota !== $oldQuota) {
$this->config->setUserValue($this->uid, 'files', 'quota', $quota);