aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/User
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/User')
-rw-r--r--lib/private/User/AvailabilityCoordinator.php126
-rw-r--r--lib/private/User/Backend.php29
-rw-r--r--lib/private/User/BackgroundJobs/CleanupDeletedUsers.php63
-rw-r--r--lib/private/User/Database.php340
-rw-r--r--lib/private/User/DisabledUserException.php10
-rw-r--r--lib/private/User/DisplayNameCache.php39
-rw-r--r--lib/private/User/LazyUser.php78
-rw-r--r--lib/private/User/Listeners/BeforeUserDeletedListener.php28
-rw-r--r--lib/private/User/Listeners/UserChangedListener.php23
-rw-r--r--lib/private/User/LoginException.php22
-rw-r--r--lib/private/User/Manager.php416
-rw-r--r--lib/private/User/NoUserException.php23
-rw-r--r--lib/private/User/OutOfOfficeData.php72
-rw-r--r--lib/private/User/PartiallyDeletedUsersBackend.php56
-rw-r--r--lib/private/User/Session.php299
-rw-r--r--lib/private/User/User.php391
16 files changed, 1253 insertions, 762 deletions
diff --git a/lib/private/User/AvailabilityCoordinator.php b/lib/private/User/AvailabilityCoordinator.php
new file mode 100644
index 00000000000..988f2e55260
--- /dev/null
+++ b/lib/private/User/AvailabilityCoordinator.php
@@ -0,0 +1,126 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\User;
+
+use JsonException;
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Service\AbsenceService;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\User\IOutOfOfficeData;
+use Psr\Log\LoggerInterface;
+
+class AvailabilityCoordinator implements IAvailabilityCoordinator {
+ private ICache $cache;
+
+ public function __construct(
+ ICacheFactory $cacheFactory,
+ private IConfig $config,
+ private AbsenceService $absenceService,
+ private LoggerInterface $logger,
+ private TimezoneService $timezoneService,
+ ) {
+ $this->cache = $cacheFactory->createLocal('OutOfOfficeData');
+ }
+
+ public function isEnabled(): bool {
+ return $this->config->getAppValue(Application::APP_ID, 'hide_absence_settings', 'no') === 'no';
+ }
+
+ private function getCachedOutOfOfficeData(IUser $user): ?OutOfOfficeData {
+ $cachedString = $this->cache->get($user->getUID());
+ if ($cachedString === null) {
+ return null;
+ }
+
+ try {
+ $cachedData = json_decode($cachedString, true, 10, JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ $this->logger->error('Failed to deserialize cached out-of-office data: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'json' => $cachedString,
+ ]);
+ return null;
+ }
+
+ return new OutOfOfficeData(
+ $cachedData['id'],
+ $user,
+ $cachedData['startDate'],
+ $cachedData['endDate'],
+ $cachedData['shortMessage'],
+ $cachedData['message'],
+ $cachedData['replacementUserId'],
+ $cachedData['replacementUserDisplayName'],
+ );
+ }
+
+ private function setCachedOutOfOfficeData(IOutOfOfficeData $data): void {
+ try {
+ $cachedString = json_encode([
+ 'id' => $data->getId(),
+ 'startDate' => $data->getStartDate(),
+ 'endDate' => $data->getEndDate(),
+ 'shortMessage' => $data->getShortMessage(),
+ 'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
+ ], JSON_THROW_ON_ERROR);
+ } catch (JsonException $e) {
+ $this->logger->error('Failed to serialize out-of-office data: ' . $e->getMessage(), [
+ 'exception' => $e,
+ ]);
+ return;
+ }
+
+ $this->cache->set($data->getUser()->getUID(), $cachedString, 300);
+ }
+
+ public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData {
+ $timezone = $this->getCachedTimezone($user->getUID());
+ if ($timezone === null) {
+ $timezone = $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone();
+ $this->setCachedTimezone($user->getUID(), $timezone);
+ }
+
+ $data = $this->getCachedOutOfOfficeData($user);
+ if ($data === null) {
+ $absenceData = $this->absenceService->getAbsence($user->getUID());
+ if ($absenceData === null) {
+ return null;
+ }
+ $data = $absenceData->toOutOufOfficeData($user, $timezone);
+ }
+
+ $this->setCachedOutOfOfficeData($data);
+ return $data;
+ }
+
+ private function getCachedTimezone(string $userId): ?string {
+ return $this->cache->get($userId . '_timezone') ?? null;
+ }
+
+ private function setCachedTimezone(string $userId, string $timezone): void {
+ $this->cache->set($userId . '_timezone', $timezone, 3600);
+ }
+
+ public function clearCache(string $userId): void {
+ $this->cache->set($userId, null, 300);
+ $this->cache->set($userId . '_timezone', null, 3600);
+ }
+
+ public function isInEffect(IOutOfOfficeData $data): bool {
+ return $this->absenceService->isInEffect($data);
+ }
+}
diff --git a/lib/private/User/Backend.php b/lib/private/User/Backend.php
index b68e4c2e541..9b6a9a890ef 100644
--- a/lib/private/User/Backend.php
+++ b/lib/private/User/Backend.php
@@ -1,26 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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;
@@ -124,9 +107,9 @@ abstract class Backend implements UserInterface {
/**
* get the user's home directory
* @param string $uid the username
- * @return boolean
+ * @return string|boolean
*/
- public function getHome($uid) {
+ public function getHome(string $uid) {
return false;
}
diff --git a/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php b/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php
new file mode 100644
index 00000000000..f999031a2a9
--- /dev/null
+++ b/lib/private/User/BackgroundJobs/CleanupDeletedUsers.php
@@ -0,0 +1,63 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\User\BackgroundJobs;
+
+use OC\User\Manager;
+use OC\User\PartiallyDeletedUsersBackend;
+use OC\User\User;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
+use Psr\Log\LoggerInterface;
+
+class CleanupDeletedUsers extends TimedJob {
+ public function __construct(
+ ITimeFactory $time,
+ private Manager $userManager,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($time);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
+ $this->setInterval(24 * 60 * 60);
+ }
+
+ protected function run($argument): void {
+ $backend = new PartiallyDeletedUsersBackend($this->config);
+ $users = $backend->getUsers();
+
+ if (empty($users)) {
+ $this->logger->debug('No failed deleted users found.');
+ return;
+ }
+
+ foreach ($users as $userId) {
+ if ($this->userManager->userExists($userId)) {
+ $this->logger->info('Skipping user {userId}, marked as deleted, as they still exists in user backend.', ['userId' => $userId]);
+ $backend->unmarkUser($userId);
+ continue;
+ }
+
+ try {
+ $user = new User(
+ $userId,
+ $backend,
+ \OCP\Server::get(IEventDispatcher::class),
+ $this->userManager,
+ $this->config,
+ );
+ $user->delete();
+ $this->logger->info('Cleaned up deleted user {userId}', ['userId' => $userId]);
+ } catch (\Throwable $error) {
+ $this->logger->warning('Could not cleanup deleted user {userId}', ['userId' => $userId, 'exception' => $error]);
+ }
+ }
+ }
+}
diff --git a/lib/private/User/Database.php b/lib/private/User/Database.php
index 8bbbccd4540..31488247939 100644
--- a/lib/private/User/Database.php
+++ b/lib/private/User/Database.php
@@ -1,61 +1,30 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author adrien <adrien.waksberg@believedigital.com>
- * @author Aldo "xoen" Giambelluca <xoen@xoen.org>
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
- * @author fabian <fabian@web2.0-apps.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Jakob Sack <mail@jakobsack.de>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Loki3000 <github@labcms.ru>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author nishiki <nishiki@yaegashi.fr>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.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 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;
use OCP\User\Backend\ISetPasswordBackend;
@@ -70,20 +39,18 @@ class Database extends ABackend implements
IGetDisplayNameBackend,
ICheckPasswordBackend,
IGetHomeBackend,
- ICountUsersBackend,
+ ILimitAwareCountUsersBackend,
ISearchKnownUsersBackend,
- IGetRealUIDBackend {
- /** @var CappedMemoryCache */
- private $cache;
-
- /** @var IEventDispatcher */
- private $eventDispatcher;
+ IGetRealUIDBackend,
+ IPasswordHashBackend {
- /** @var IDBConnection */
- private $dbConn;
+ private CappedMemoryCache $cache;
+ private IConfig $config;
+ private ?IDBConnection $dbConnection;
+ private IEventDispatcher $eventDispatcher;
+ private string $table;
- /** @var string */
- private $table;
+ use TTransactional;
/**
* \OC\User\Database constructor.
@@ -94,16 +61,19 @@ class Database extends ABackend implements
public function __construct($eventDispatcher = null, $table = 'users') {
$this->cache = new CappedMemoryCache();
$this->table = $table;
- $this->eventDispatcher = $eventDispatcher ? $eventDispatcher : \OC::$server->query(IEventDispatcher::class);
+ $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;
}
/**
@@ -117,48 +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)) {
+ return false;
+ }
- if (!$this->userExists($uid)) {
- $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
+ $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
- $qb = $this->dbConn->getQueryBuilder();
+ $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(\OC::$server->getHasher()->hash($password)),
+ 'password' => $qb->createNamedParameter(\OCP\Server::get(IHasher::class)->hash($password)),
'uid_lower' => $qb->createNamedParameter(mb_strtolower($uid)),
]);
- $result = $qb->execute();
+ $result = $qb->executeStatement();
// Clear cache
unset($this->cache[$uid]);
+ // Repopulate the cache
+ $this->loadUser($uid);
- return $result ? true : false;
- }
-
- return false;
+ 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->execute();
+ $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]);
}
@@ -166,11 +142,12 @@ 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))));
- $result = $query->execute();
+ $result = $query->executeStatement();
return $result ? true : false;
}
@@ -185,24 +162,59 @@ 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)) {
+ return false;
+ }
- if ($this->userExists($uid)) {
- $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
+ $this->eventDispatcher->dispatchTyped(new ValidatePasswordPolicyEvent($password));
- $hasher = \OC::$server->getHasher();
- $hashedPassword = $hasher->hash($password);
+ $hasher = \OCP\Server::get(IHasher::class);
+ $hashedPassword = $hasher->hash($password);
- $return = $this->updatePassword($uid, $hashedPassword);
+ $return = $this->updatePassword($uid, $hashedPassword);
- if ($return) {
- $this->cache[$uid]['password'] = $hashedPassword;
- }
+ if ($return) {
+ $this->cache[$uid]['password'] = $hashedPassword;
+ }
+
+ return $return;
+ }
- return $return;
+ public function getPasswordHash(string $userId): ?string {
+ if (!$this->userExists($userId)) {
+ return null;
+ }
+ if (!empty($this->cache[$userId]['password'])) {
+ return $this->cache[$userId]['password'];
}
- return false;
+ $dbConn = $this->getDbConnection();
+ $qb = $dbConn->getQueryBuilder();
+ $qb->select('password')
+ ->from($this->table)
+ ->where($qb->expr()->eq('uid_lower', $qb->createNamedParameter(mb_strtolower($userId))));
+ /** @var false|string $hash */
+ $hash = $qb->executeQuery()->fetchOne();
+ if ($hash === false) {
+ return null;
+ }
+
+ $this->cache[$userId]['password'] = $hash;
+ return $hash;
+ }
+
+ public function setPasswordHash(string $userId, string $passwordHash): bool {
+ if (!\OCP\Server::get(IHasher::class)->validate($passwordHash)) {
+ throw new InvalidArgumentException();
+ }
+
+ $result = $this->updatePassword($userId, $passwordHash);
+ if (!$result) {
+ return false;
+ }
+
+ $this->cache[$userId]['password'] = $passwordHash;
+ return true;
}
/**
@@ -221,21 +233,20 @@ class Database extends ABackend implements
throw new \InvalidArgumentException('Invalid displayname');
}
- $this->fixDI();
+ if (!$this->userExists($uid)) {
+ return false;
+ }
- 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->execute();
+ $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();
- $this->cache[$uid]['displayname'] = $displayName;
+ $this->cache[$uid]['displayname'] = $displayName;
- return true;
- }
-
- return false;
+ return true;
}
/**
@@ -261,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')
@@ -273,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)
@@ -301,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')
@@ -313,15 +322,15 @@ 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')
->setMaxResults($limit)
->setFirstResult($offset);
- $result = $query->execute();
+ $result = $query->executeQuery();
$displayNames = [];
while ($row = $result->fetch()) {
$displayNames[(string)$row['uid']] = (string)$row['displayname'];
@@ -333,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
*
@@ -346,7 +355,7 @@ class Database extends ABackend implements
if ($found && is_array($this->cache[$loginName])) {
$storedHash = $this->cache[$loginName]['password'];
$newHash = '';
- if (\OC::$server->getHasher()->verify($password, $storedHash, $newHash)) {
+ if (\OCP\Server::get(IHasher::class)->verify($password, $storedHash, $newHash)) {
if (!empty($newHash)) {
$this->updatePassword($loginName, $newHash);
}
@@ -360,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;
+ }
- $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->execute();
- $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;
+ $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;
+ }
+
+ // 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;
}
/**
@@ -428,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);
}
/**
@@ -440,7 +466,7 @@ class Database extends ABackend implements
*/
public function getHome(string $uid) {
if ($this->userExists($uid)) {
- return \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $uid;
+ return $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $uid;
}
return false;
@@ -455,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;
}
/**
@@ -497,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 5d1cc8940d7..4321d95f88e 100644
--- a/lib/private/User/DisplayNameCache.php
+++ b/lib/private/User/DisplayNameCache.php
@@ -1,34 +1,20 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
- * @license AGPL-3.0-or-later
- *
- * 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: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
-
namespace OC\User;
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;
/**
* Class that cache the relation UserId -> Display name
@@ -36,8 +22,11 @@ use OCP\User\Events\UserChangedEvent;
* This saves fetching the user from a user backend and later on fetching
* their preferences. It's generally not an issue if this data is slightly
* outdated.
+ * @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;
@@ -51,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;
@@ -64,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;
}
@@ -79,7 +73,12 @@ 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();
+ unset($this->cache[$userId]);
+ $this->memCache->remove($userId);
}
}
}
diff --git a/lib/private/User/LazyUser.php b/lib/private/User/LazyUser.php
index 096578b8f37..501169019d4 100644
--- a/lib/private/User/LazyUser.php
+++ b/lib/private/User/LazyUser.php
@@ -2,23 +2,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\User;
@@ -30,20 +15,33 @@ use OCP\UserInterface;
class LazyUser implements IUser {
private ?IUser $user = null;
private string $uid;
+ private ?string $displayName;
private IUserManager $userManager;
+ private ?UserInterface $backend;
- public function __construct(string $uid, IUserManager $userManager) {
+ public function __construct(string $uid, IUserManager $userManager, ?string $displayName = null, ?UserInterface $backend = null) {
$this->uid = $uid;
$this->userManager = $userManager;
+ $this->displayName = $displayName;
+ $this->backend = $backend;
}
private function getUser(): IUser {
if ($this->user === null) {
- $this->user = $this->userManager->get($this->uid);
+ if ($this->backend) {
+ /** @var \OC\User\Manager $manager */
+ $manager = $this->userManager;
+ $this->user = $manager->getUserObject($this->uid, $this->backend);
+ } else {
+ $this->user = $this->userManager->get($this->uid);
+ }
}
- /** @var IUser */
- $user = $this->user;
- return $user;
+
+ if ($this->user === null) {
+ throw new NoUserException('User not found in backend');
+ }
+
+ return $this->user;
}
public function getUID() {
@@ -51,6 +49,10 @@ class LazyUser implements IUser {
}
public function getDisplayName() {
+ if ($this->displayName) {
+ return $this->displayName;
+ }
+
return $this->userManager->getDisplayName($this->uid) ?? $this->uid;
}
@@ -58,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();
}
@@ -74,6 +80,14 @@ class LazyUser implements IUser {
return $this->getUser()->setPassword($password, $recoveryPassword);
}
+ public function getPasswordHash(): ?string {
+ return $this->getUser()->getPasswordHash();
+ }
+
+ public function setPasswordHash(string $passwordHash): bool {
+ return $this->getUser()->setPasswordHash($passwordHash);
+ }
+
public function getHome() {
return $this->getUser()->getHome();
}
@@ -98,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();
}
@@ -142,7 +160,19 @@ 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);
}
+
+ public function getManagerUids(): array {
+ return $this->getUser()->getManagerUids();
+ }
+
+ public function setManagerUids(array $uids): void {
+ $this->getUser()->setManagerUids($uids);
+ }
}
diff --git a/lib/private/User/Listeners/BeforeUserDeletedListener.php b/lib/private/User/Listeners/BeforeUserDeletedListener.php
index ec1f80c5413..50dc9835400 100644
--- a/lib/private/User/Listeners/BeforeUserDeletedListener.php
+++ b/lib/private/User/Listeners/BeforeUserDeletedListener.php
@@ -3,31 +3,17 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\User\Listeners;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
-use OCP\User\Events\BeforeUserDeletedEvent;
use OCP\Files\NotFoundException;
use OCP\IAvatarManager;
+use OCP\Security\ICredentialsManager;
+use OCP\User\Events\BeforeUserDeletedEvent;
use Psr\Log\LoggerInterface;
/**
@@ -35,10 +21,12 @@ use Psr\Log\LoggerInterface;
*/
class BeforeUserDeletedListener implements IEventListener {
private IAvatarManager $avatarManager;
+ private ICredentialsManager $credentialsManager;
private LoggerInterface $logger;
- public function __construct(LoggerInterface $logger, IAvatarManager $avatarManager) {
+ public function __construct(LoggerInterface $logger, IAvatarManager $avatarManager, ICredentialsManager $credentialsManager) {
$this->avatarManager = $avatarManager;
+ $this->credentialsManager = $credentialsManager;
$this->logger = $logger;
}
@@ -61,5 +49,7 @@ class BeforeUserDeletedListener implements IEventListener {
'exception' => $e,
]);
}
+ // Delete storages credentials on user deletion
+ $this->credentialsManager->erase($user->getUID());
}
}
diff --git a/lib/private/User/Listeners/UserChangedListener.php b/lib/private/User/Listeners/UserChangedListener.php
index a561db2423d..8f618950255 100644
--- a/lib/private/User/Listeners/UserChangedListener.php
+++ b/lib/private/User/Listeners/UserChangedListener.php
@@ -3,31 +3,16 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\User\Listeners;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
-use OCP\User\Events\UserChangedEvent;
use OCP\Files\NotFoundException;
use OCP\IAvatarManager;
+use OCP\User\Events\UserChangedEvent;
/**
* @template-implements IEventListener<UserChangedEvent>
@@ -43,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/LoginException.php b/lib/private/User/LoginException.php
index 7133b7b76dc..6f0e98513dd 100644
--- a/lib/private/User/LoginException.php
+++ b/lib/private/User/LoginException.php
@@ -1,23 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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;
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);
+ }
}
diff --git a/lib/private/User/NoUserException.php b/lib/private/User/NoUserException.php
index d9149318467..c0a5c51b08d 100644
--- a/lib/private/User/NoUserException.php
+++ b/lib/private/User/NoUserException.php
@@ -1,24 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- *
- * @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;
diff --git a/lib/private/User/OutOfOfficeData.php b/lib/private/User/OutOfOfficeData.php
new file mode 100644
index 00000000000..0d4e142567e
--- /dev/null
+++ b/lib/private/User/OutOfOfficeData.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\User;
+
+use OCP\IUser;
+use OCP\User\IOutOfOfficeData;
+
+class OutOfOfficeData implements IOutOfOfficeData {
+ public function __construct(
+ private string $id,
+ private IUser $user,
+ private int $startDate,
+ private int $endDate,
+ private string $shortMessage,
+ private string $message,
+ private ?string $replacementUserId,
+ private ?string $replacementUserDisplayName,
+ ) {
+ }
+
+ public function getId(): string {
+ return $this->id;
+ }
+
+ public function getUser(): IUser {
+ return $this->user;
+ }
+
+ public function getStartDate(): int {
+ return $this->startDate;
+ }
+
+ public function getEndDate(): int {
+ return $this->endDate;
+ }
+
+ public function getShortMessage(): string {
+ return $this->shortMessage;
+ }
+
+ public function getMessage(): string {
+ return $this->message;
+ }
+
+ public function getReplacementUserId(): ?string {
+ return $this->replacementUserId;
+ }
+
+ public function getReplacementUserDisplayName(): ?string {
+ return $this->replacementUserDisplayName;
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->getId(),
+ 'userId' => $this->getUser()->getUID(),
+ 'startDate' => $this->getStartDate(),
+ 'endDate' => $this->getEndDate(),
+ 'shortMessage' => $this->getShortMessage(),
+ 'message' => $this->getMessage(),
+ 'replacementUserId' => $this->getReplacementUserId(),
+ 'replacementUserDisplayName' => $this->getReplacementUserDisplayName(),
+ ];
+ }
+}
diff --git a/lib/private/User/PartiallyDeletedUsersBackend.php b/lib/private/User/PartiallyDeletedUsersBackend.php
new file mode 100644
index 00000000000..298ddaff6c6
--- /dev/null
+++ b/lib/private/User/PartiallyDeletedUsersBackend.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\User;
+
+use OCP\IConfig;
+use OCP\IUserBackend;
+use OCP\User\Backend\IGetHomeBackend;
+
+/**
+ * This is a "fake" backend for users that were deleted,
+ * but not properly removed from Nextcloud (e.g. an exception occurred).
+ * This backend is only needed because some APIs in user-deleted-events require a "real" user with backend.
+ */
+class PartiallyDeletedUsersBackend extends Backend implements IGetHomeBackend, IUserBackend {
+
+ public function __construct(
+ private IConfig $config,
+ ) {
+ }
+
+ public function deleteUser($uid): bool {
+ // fake true, deleting failed users is automatically handled by User::delete()
+ return true;
+ }
+
+ public function getBackendName(): string {
+ return 'deleted users';
+ }
+
+ public function userExists($uid) {
+ return $this->config->getUserValue($uid, 'core', 'deleted') === 'true';
+ }
+
+ public function getHome(string $uid): string|false {
+ return $this->config->getUserValue($uid, 'core', 'deleted.home-path') ?: false;
+ }
+
+ public function getUsers($search = '', $limit = null, $offset = null) {
+ return $this->config->getUsersForUserValue('core', 'deleted', 'true');
+ }
+
+ /**
+ * Unmark a user as deleted.
+ * This typically the case if the user deletion failed in the backend but before the backend deleted the user,
+ * meaning the user still exists so we unmark them as it still can be accessed (and deleted) normally.
+ */
+ public function unmarkUser(string $userId): void {
+ $this->config->deleteUserValue($userId, 'core', 'deleted');
+ $this->config->deleteUserValue($userId, 'core', 'deleted.home-path');
+ }
+
+}
diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php
index c7b11e22504..e7bfcf56407 100644
--- a/lib/private/User/Session.php
+++ b/lib/private/User/Session.php
@@ -1,70 +1,46 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Sandro Lutz <sandro.lutz@temparus.ch>
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bernhard Posselt <dev@bernhard-posselt.com>
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Felix Rupp <github@felixrupp.com>
- * @author Greta Doci <gretadoci@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lionel Elie Mamane <lionel@mamane.lu>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sandro Lutz <sandro.lutz@temparus.ch>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <vincent@nextcloud.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: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\User;
use OC;
-use OC\Authentication\Exceptions\ExpiredTokenException;
-use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
+use OC\Authentication\Token\PublicKeyToken;
+use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager;
use OC\Hooks\Emitter;
use OC\Hooks\PublicEmitter;
+use OC\Security\CSRF\CsrfTokenManager;
use OC_User;
use OC_Util;
use OCA\DAV\Connector\Sabre\Auth;
+use OCP\AppFramework\Db\TTransactional;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Authentication\Exceptions\ExpiredTokenException;
+use OCP\Authentication\Exceptions\InvalidTokenException;
+use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
+use OCP\IDBConnection;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Lockdown\ILockdownManager;
+use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
use OCP\User\Events\PostLoginEvent;
+use OCP\User\Events\UserFirstTimeLoggedInEvent;
use OCP\Util;
use Psr\Log\LoggerInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
/**
* Class Session
@@ -89,53 +65,22 @@ use Symfony\Component\EventDispatcher\GenericEvent;
* @package OC\User
*/
class Session implements IUserSession, Emitter {
- /** @var Manager $manager */
- private $manager;
-
- /** @var ISession $session */
- private $session;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var IProvider */
- private $tokenProvider;
-
- /** @var IConfig */
- private $config;
+ use TTransactional;
/** @var User $activeUser */
protected $activeUser;
- /** @var ISecureRandom */
- private $random;
-
- /** @var ILockdownManager */
- private $lockdownManager;
-
- private LoggerInterface $logger;
- /** @var IEventDispatcher */
- private $dispatcher;
-
- public function __construct(Manager $manager,
- ISession $session,
- ITimeFactory $timeFactory,
- ?IProvider $tokenProvider,
- IConfig $config,
- ISecureRandom $random,
- ILockdownManager $lockdownManager,
- LoggerInterface $logger,
- IEventDispatcher $dispatcher
+ public function __construct(
+ private Manager $manager,
+ private ISession $session,
+ private ITimeFactory $timeFactory,
+ private ?IProvider $tokenProvider,
+ private IConfig $config,
+ private ISecureRandom $random,
+ private ILockdownManager $lockdownManager,
+ private LoggerInterface $logger,
+ private IEventDispatcher $dispatcher,
) {
- $this->manager = $manager;
- $this->session = $session;
- $this->timeFactory = $timeFactory;
- $this->tokenProvider = $tokenProvider;
- $this->config = $config;
- $this->random = $random;
- $this->lockdownManager = $lockdownManager;
- $this->logger = $logger;
- $this->dispatcher = $dispatcher;
}
/**
@@ -159,7 +104,7 @@ class Session implements IUserSession, Emitter {
* @param string $method optional
* @param callable $callback optional
*/
- public function removeListener($scope = null, $method = null, callable $callback = null) {
+ public function removeListener($scope = null, $method = null, ?callable $callback = null) {
$this->manager->removeListener($scope, $method, $callback);
}
@@ -209,6 +154,15 @@ class Session implements IUserSession, Emitter {
}
/**
+ * Temporarily set the currently active user without persisting in the session
+ *
+ * @param IUser|null $user
+ */
+ public function setVolatileActiveUser(?IUser $user): void {
+ $this->activeUser = $user;
+ }
+
+ /**
* get the current active user
*
* @return IUser|null Current user, otherwise null
@@ -365,8 +319,8 @@ class Session implements IUserSession, Emitter {
if (!$user->isEnabled()) {
// disabled users can not log in
// injecting l10n does not work - there is a circular dependency between session and \OCP\L10N\IFactory
- $message = \OC::$server->getL10N('lib')->t('User disabled');
- throw new LoginException($message);
+ $message = \OCP\Util::getL10N('lib')->t('Account disabled');
+ throw new DisabledUserException($message);
}
if ($regenerateSessionId) {
@@ -381,6 +335,7 @@ class Session implements IUserSession, Emitter {
if ($isToken) {
$this->setToken($loginDetails['token']->getId());
$this->lockdownManager->setToken($loginDetails['token']);
+ $user->updateLastLoginTimestamp();
$firstTimeLogin = false;
} else {
$this->setToken(null);
@@ -404,7 +359,7 @@ class Session implements IUserSession, Emitter {
return true;
}
- $message = \OC::$server->getL10N('lib')->t('Login canceled by app');
+ $message = \OCP\Util::getL10N('lib')->t('Login canceled by app');
throw new LoginException($message);
}
@@ -417,16 +372,17 @@ class Session implements IUserSession, Emitter {
* @param string $user
* @param string $password
* @param IRequest $request
- * @param OC\Security\Bruteforce\Throttler $throttler
+ * @param IThrottler $throttler
* @throws LoginException
* @throws PasswordLoginForbiddenException
* @return boolean
*/
public function logClientIn($user,
- $password,
- IRequest $request,
- OC\Security\Bruteforce\Throttler $throttler) {
- $currentDelay = $throttler->sleepDelay($request->getRemoteAddress(), 'login');
+ $password,
+ IRequest $request,
+ IThrottler $throttler) {
+ $remoteAddress = $request->getRemoteAddress();
+ $currentDelay = $throttler->sleepDelayOrThrowOnMax($remoteAddress, 'login');
if ($this->manager instanceof PublicEmitter) {
$this->manager->emit('\OC\User', 'preLogin', [$user, $password]);
@@ -450,19 +406,22 @@ class Session implements IUserSession, Emitter {
if (!$this->login($user, $password)) {
// Failed, maybe the user used their email address
if (!filter_var($user, FILTER_VALIDATE_EMAIL)) {
+ $this->handleLoginFailed($throttler, $currentDelay, $remoteAddress, $user, $password);
return false;
}
- $users = $this->manager->getByEmail($user);
- if (!(\count($users) === 1 && $this->login($users[0]->getUID(), $password))) {
- $this->logger->warning('Login failed: \'' . $user . '\' (Remote IP: \'' . \OC::$server->getRequest()->getRemoteAddress() . '\')', ['app' => 'core']);
-
- $throttler->registerAttempt('login', $request->getRemoteAddress(), ['user' => $user]);
- $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user, $password));
+ if ($isTokenPassword) {
+ $dbToken = $this->tokenProvider->getToken($password);
+ $userFromToken = $this->manager->get($dbToken->getUID());
+ $isValidEmailLogin = $userFromToken->getEMailAddress() === $user
+ && $this->validateTokenLoginName($userFromToken->getEMailAddress(), $dbToken);
+ } else {
+ $users = $this->manager->getByEmail($user);
+ $isValidEmailLogin = (\count($users) === 1 && $this->login($users[0]->getUID(), $password));
+ }
- if ($currentDelay === 0) {
- $throttler->sleepDelay($request->getRemoteAddress(), 'login');
- }
+ if (!$isValidEmailLogin) {
+ $this->handleLoginFailed($throttler, $currentDelay, $remoteAddress, $user, $password);
return false;
}
}
@@ -477,6 +436,17 @@ class Session implements IUserSession, Emitter {
return true;
}
+ private function handleLoginFailed(IThrottler $throttler, int $currentDelay, string $remoteAddress, string $user, ?string $password) {
+ $this->logger->warning("Login failed: '" . $user . "' (Remote IP: '" . $remoteAddress . "')", ['app' => 'core']);
+
+ $throttler->registerAttempt('login', $remoteAddress, ['user' => $user]);
+ $this->dispatcher->dispatchTyped(new OC\Authentication\Events\LoginFailed($user, $password));
+
+ if ($currentDelay === 0) {
+ $throttler->sleepDelayOrThrowOnMax($remoteAddress, 'login');
+ }
+ }
+
protected function supportsCookies(IRequest $request) {
if (!is_null($request->getCookie('cookie_test'))) {
return true;
@@ -485,8 +455,8 @@ class Session implements IUserSession, Emitter {
return false;
}
- private function isTokenAuthEnforced() {
- return $this->config->getSystemValue('token_auth_enforced', false);
+ private function isTokenAuthEnforced(): bool {
+ return $this->config->getSystemValueBool('token_auth_enforced', false);
}
protected function isTwoFactorEnforced($username) {
@@ -507,7 +477,7 @@ class Session implements IUserSession, Emitter {
$user = $users[0];
}
// DI not possible due to cyclic dependencies :'-/
- return OC::$server->getTwoFactorAuthManager()->isTwoFactorAuthenticated($user);
+ return OC::$server->get(TwoFactorAuthManager::class)->isTwoFactorAuthenticated($user);
}
/**
@@ -535,7 +505,7 @@ class Session implements IUserSession, Emitter {
if ($refreshCsrfToken) {
// TODO: mock/inject/use non-static
// Refresh the token
- \OC::$server->getCsrfTokenManager()->refreshToken();
+ \OC::$server->get(CsrfTokenManager::class)->refreshToken();
}
if ($firstTimeLogin) {
@@ -555,7 +525,8 @@ class Session implements IUserSession, Emitter {
}
// trigger any other initialization
- \OC::$server->getEventDispatcher()->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
+ \OC::$server->get(IEventDispatcher::class)->dispatch(IUser::class . '::firstLogin', new GenericEvent($this->getUser()));
+ \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new UserFirstTimeLoggedInEvent($this->getUser()));
}
}
@@ -564,11 +535,11 @@ class Session implements IUserSession, Emitter {
*
* @todo do not allow basic auth if the user is 2FA enforced
* @param IRequest $request
- * @param OC\Security\Bruteforce\Throttler $throttler
+ * @param IThrottler $throttler
* @return boolean if the login was successful
*/
public function tryBasicAuthLogin(IRequest $request,
- OC\Security\Bruteforce\Throttler $throttler) {
+ IThrottler $throttler) {
if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) {
try {
if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
@@ -676,8 +647,10 @@ class Session implements IUserSession, Emitter {
$sessionId = $this->session->getId();
$pwd = $this->getPassword($password);
// Make sure the current sessionId has no leftover tokens
- $this->tokenProvider->invalidateToken($sessionId);
- $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember);
+ $this->atomic(function () use ($sessionId, $uid, $loginName, $pwd, $name, $remember) {
+ $this->tokenProvider->invalidateToken($sessionId);
+ $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, $remember);
+ }, \OCP\Server::get(IDBConnection::class));
return true;
} catch (SessionNotAvailableException $ex) {
// This can happen with OCC, where a memory session is used
@@ -739,8 +712,6 @@ class Session implements IUserSession, Emitter {
return false;
}
- $dbToken->setLastCheck($now);
- $this->tokenProvider->updateToken($dbToken);
return true;
}
@@ -758,6 +729,9 @@ class Session implements IUserSession, Emitter {
}
$dbToken->setLastCheck($now);
+ if ($dbToken instanceof PublicKeyToken) {
+ $dbToken->setLastActivity($now);
+ }
$this->tokenProvider->updateToken($dbToken);
return true;
}
@@ -775,23 +749,23 @@ class Session implements IUserSession, Emitter {
try {
$dbToken = $this->tokenProvider->getToken($token);
} catch (InvalidTokenException $ex) {
+ $this->logger->debug('Session token is invalid because it does not exist', [
+ 'app' => 'core',
+ 'user' => $user,
+ 'exception' => $ex,
+ ]);
return false;
}
- // Check if login names match
- if (!is_null($user) && $dbToken->getLoginName() !== $user) {
- // 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'.
- $this->logger->error('App token login name does not match', [
- 'tokenLoginName' => $dbToken->getLoginName(),
- 'sessionLoginName' => $user,
- ]);
-
+ if (!is_null($user) && !$this->validateTokenLoginName($user, $dbToken)) {
return false;
}
if (!$this->checkTokenCredentials($dbToken, $token)) {
+ $this->logger->warning('Session token credentials are invalid', [
+ 'app' => 'core',
+ 'user' => $user,
+ ]);
return false;
}
@@ -804,6 +778,27 @@ class Session implements IUserSession, Emitter {
}
/**
+ * Check if login names match
+ */
+ private function validateTokenLoginName(?string $loginName, IToken $token): bool {
+ 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'.
+ $this->logger->error('App token login name does not match', [
+ 'tokenLoginName' => $token->getLoginName(),
+ 'sessionLoginName' => $loginName,
+ 'app' => 'core',
+ 'user' => $token->getUID(),
+ ]);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
* Tries to login the user with auth token header
*
* @param IRequest $request
@@ -812,15 +807,18 @@ class Session implements IUserSession, Emitter {
*/
public function tryTokenLogin(IRequest $request) {
$authHeader = $request->getHeader('Authorization');
- if (strpos($authHeader, 'Bearer ') === 0) {
+ if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
- } else {
- // No auth header, let's try session id
+ } elseif ($request->getCookie($this->config->getSystemValueString('instanceid')) !== null) {
+ // No auth header, let's try session id, but only if this is an existing
+ // session and the request has a session cookie
try {
$token = $this->session->getId();
} catch (SessionNotAvailableException $ex) {
return false;
}
+ } else {
+ return false;
}
if (!$this->loginWithToken($token)) {
@@ -837,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);
}
@@ -867,9 +864,9 @@ class Session implements IUserSession, Emitter {
$tokens = $this->config->getUserKeys($uid, 'login_token');
// test cookies token against stored tokens
if (!in_array($currentToken, $tokens, true)) {
- $this->logger->info('Tried to log in {uid} but could not verify token', [
+ $this->logger->info('Tried to log in but could not verify token', [
'app' => 'core',
- 'uid' => $uid,
+ 'user' => $uid,
]);
return false;
}
@@ -877,18 +874,31 @@ class Session implements IUserSession, Emitter {
$this->config->deleteUserValue($uid, 'login_token', $currentToken);
$newToken = $this->random->generate(32);
$this->config->setUserValue($uid, 'login_token', $newToken, (string)$this->timeFactory->getTime());
+ $this->logger->debug('Remember-me token replaced', [
+ 'app' => 'core',
+ 'user' => $uid,
+ ]);
try {
$sessionId = $this->session->getId();
$token = $this->tokenProvider->renewSessionToken($oldSessionId, $sessionId);
+ $this->logger->debug('Session token replaced', [
+ 'app' => 'core',
+ 'user' => $uid,
+ ]);
} catch (SessionNotAvailableException $ex) {
- $this->logger->warning('Could not renew session token for {uid} because the session is unavailable', [
+ $this->logger->critical('Could not renew session token for {uid} because the session is unavailable', [
'app' => 'core',
'uid' => $uid,
+ 'user' => $uid,
]);
return false;
} catch (InvalidTokenException $ex) {
- $this->logger->warning('Renewing session token failed', ['app' => 'core']);
+ $this->logger->error('Renewing session token failed: ' . $ex->getMessage(), [
+ 'app' => 'core',
+ 'user' => $uid,
+ 'exception' => $ex,
+ ]);
return false;
}
@@ -927,10 +937,17 @@ class Session implements IUserSession, Emitter {
$this->manager->emit('\OC\User', 'logout', [$user]);
if ($user !== null) {
try {
- $this->tokenProvider->invalidateToken($this->session->getId());
+ $token = $this->session->getId();
+ $this->tokenProvider->invalidateToken($token);
+ $this->logger->debug('Session token invalidated before logout', [
+ 'user' => $user->getUID(),
+ ]);
} catch (SessionNotAvailableException $ex) {
}
}
+ $this->logger->debug('Logging out', [
+ 'user' => $user === null ? null : $user->getUID(),
+ ]);
$this->setUser(null);
$this->setLoginName(null);
$this->setToken(null);
@@ -951,14 +968,15 @@ class Session implements IUserSession, Emitter {
if ($webRoot === '') {
$webRoot = '/';
}
+ $domain = $this->config->getSystemValueString('cookie_domain');
- $maxAge = $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
+ $maxAge = $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
\OC\Http\CookieHelper::setCookie(
'nc_username',
$username,
$maxAge,
$webRoot,
- '',
+ $domain,
$secureCookie,
true,
\OC\Http\CookieHelper::SAMESITE_LAX
@@ -968,7 +986,7 @@ class Session implements IUserSession, Emitter {
$token,
$maxAge,
$webRoot,
- '',
+ $domain,
$secureCookie,
true,
\OC\Http\CookieHelper::SAMESITE_LAX
@@ -979,7 +997,7 @@ class Session implements IUserSession, Emitter {
$this->session->getId(),
$maxAge,
$webRoot,
- '',
+ $domain,
$secureCookie,
true,
\OC\Http\CookieHelper::SAMESITE_LAX
@@ -995,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 7044770b57e..1f908918b20 100644
--- a/lib/private/User/User.php
+++ b/lib/private/User/User.php
@@ -1,36 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Leon Klingele <leon@struktur.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>
- *
- * @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;
@@ -38,81 +11,76 @@ 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;
use OCP\Group\Events\BeforeUserRemovedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\IAvatarManager;
use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
use OCP\IImage;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserBackend;
+use OCP\Notification\IManager as INotificationManager;
+use OCP\User\Backend\IGetHomeBackend;
+use OCP\User\Backend\IPasswordHashBackend;
+use OCP\User\Backend\IProvideAvatarBackend;
+use OCP\User\Backend\IProvideEnabledStateBackend;
+use OCP\User\Backend\ISetDisplayNameBackend;
+use OCP\User\Backend\ISetPasswordBackend;
+use OCP\User\Events\BeforePasswordUpdatedEvent;
use OCP\User\Events\BeforeUserDeletedEvent;
+use OCP\User\Events\PasswordUpdatedEvent;
+use OCP\User\Events\UserChangedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\User\GetQuotaEvent;
-use OCP\User\Backend\ISetDisplayNameBackend;
-use OCP\User\Backend\ISetPasswordBackend;
-use OCP\User\Backend\IProvideAvatarBackend;
-use OCP\User\Backend\IGetHomeBackend;
use OCP\UserInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
+use Psr\Log\LoggerInterface;
+
+use function json_decode;
+use function json_encode;
class User implements IUser {
+ private const CONFIG_KEY_MANAGERS = 'manager';
+
+ private IConfig $config;
+ private IURLGenerator $urlGenerator;
+
/** @var IAccountManager */
protected $accountManager;
- /** @var string */
- private $uid;
/** @var string|null */
private $displayName;
- /** @var UserInterface|null */
- private $backend;
- /** @var EventDispatcherInterface */
- private $legacyDispatcher;
-
- /** @var IEventDispatcher */
- private $dispatcher;
-
/** @var bool|null */
private $enabled;
- /** @var Emitter|Manager */
+ /** @var Emitter|Manager|null */
private $emitter;
/** @var string */
private $home;
- /** @var int|null */
- private $lastLogin;
-
- /** @var \OCP\IConfig */
- private $config;
+ private ?int $lastLogin = null;
+ private ?int $firstLogin = null;
/** @var IAvatarManager */
private $avatarManager;
- /** @var IURLGenerator */
- private $urlGenerator;
-
- public function __construct(string $uid, ?UserInterface $backend, EventDispatcherInterface $dispatcher, $emitter = null, IConfig $config = null, $urlGenerator = null) {
- $this->uid = $uid;
- $this->backend = $backend;
- $this->legacyDispatcher = $dispatcher;
+ public function __construct(
+ private string $uid,
+ private ?UserInterface $backend,
+ private IEventDispatcher $dispatcher,
+ $emitter = null,
+ ?IConfig $config = null,
+ $urlGenerator = null,
+ ) {
$this->emitter = $emitter;
- if (is_null($config)) {
- $config = \OC::$server->getConfig();
- }
- $this->config = $config;
- $this->urlGenerator = $urlGenerator;
- if (is_null($this->urlGenerator)) {
- $this->urlGenerator = \OC::$server->getURLGenerator();
- }
- // TODO: inject
- $this->dispatcher = \OC::$server->query(IEventDispatcher::class);
+ $this->config = $config ?? \OCP\Server::get(IConfig::class);
+ $this->urlGenerator = $urlGenerator ?? \OCP\Server::get(IURLGenerator::class);
}
/**
@@ -186,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');
@@ -208,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;
@@ -233,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);
+ $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;
@@ -266,54 +255,85 @@ class User implements IUser {
* @return bool
*/
public function delete() {
- /** @deprecated 21.0.0 use BeforeUserDeletedEvent event with the IEventDispatcher instead */
- $this->legacyDispatcher->dispatch(IUser::class . '::preDelete', new GenericEvent($this));
+ if ($this->backend === null) {
+ \OCP\Server::get(LoggerInterface::class)->error('Cannot delete user: No backend set');
+ return false;
+ }
+
if ($this->emitter) {
/** @deprecated 21.0.0 use BeforeUserDeletedEvent event with the IEventDispatcher instead */
$this->emitter->emit('\OC\User', 'preDelete', [$this]);
}
$this->dispatcher->dispatchTyped(new BeforeUserDeletedEvent($this));
- $result = $this->backend->deleteUser($this->uid);
- if ($result) {
- // FIXME: Feels like an hack - suggestions?
-
- $groupManager = \OC::$server->getGroupManager();
- // We have to delete the user from all groups
- foreach ($groupManager->getUserGroupIds($this) as $groupId) {
- $group = $groupManager->get($groupId);
- if ($group) {
- $this->dispatcher->dispatchTyped(new BeforeUserRemovedEvent($group, $this));
- $group->removeUser($this);
- $this->dispatcher->dispatchTyped(new UserRemovedEvent($group, $this));
- }
- }
- // Delete the user's keys in preferences
- \OC::$server->getConfig()->deleteAllUserValues($this->uid);
- \OC::$server->getCommentsManager()->deleteReferencesOfActor('users', $this->uid);
- \OC::$server->getCommentsManager()->deleteReadMarksFromUser($this);
+ // Set delete flag on the user - this is needed to ensure that the user data is removed if there happen any exception in the backend
+ // because we can not restore the user meaning we could not rollback to any stable state otherwise.
+ $this->config->setUserValue($this->uid, 'core', 'deleted', 'true');
+ // We also need to backup the home path as this can not be reconstructed later if the original backend uses custom home paths
+ $this->config->setUserValue($this->uid, 'core', 'deleted.home-path', $this->getHome());
- /** @var AvatarManager $avatarManager */
- $avatarManager = \OC::$server->query(AvatarManager::class);
- $avatarManager->deleteUserAvatar($this->uid);
+ // Try to delete the user on the backend
+ $result = $this->backend->deleteUser($this->uid);
+ if ($result === false) {
+ // The deletion was aborted or something else happened, we are in a defined state, so remove the delete flag
+ $this->config->deleteUserValue($this->uid, 'core', 'deleted');
+ return false;
+ }
- $notification = \OC::$server->getNotificationManager()->createNotification();
- $notification->setUser($this->uid);
- \OC::$server->getNotificationManager()->markProcessed($notification);
+ // We have to delete the user from all groups
+ $groupManager = \OCP\Server::get(IGroupManager::class);
+ foreach ($groupManager->getUserGroupIds($this) as $groupId) {
+ $group = $groupManager->get($groupId);
+ if ($group) {
+ $this->dispatcher->dispatchTyped(new BeforeUserRemovedEvent($group, $this));
+ $group->removeUser($this);
+ $this->dispatcher->dispatchTyped(new UserRemovedEvent($group, $this));
+ }
+ }
- /** @var AccountManager $accountManager */
- $accountManager = \OC::$server->query(AccountManager::class);
- $accountManager->deleteUser($this);
+ $commentsManager = \OCP\Server::get(ICommentsManager::class);
+ $commentsManager->deleteReferencesOfActor('users', $this->uid);
+ $commentsManager->deleteReadMarksFromUser($this);
+
+ $avatarManager = \OCP\Server::get(AvatarManager::class);
+ $avatarManager->deleteUserAvatar($this->uid);
+
+ $notificationManager = \OCP\Server::get(INotificationManager::class);
+ $notification = $notificationManager->createNotification();
+ $notification->setUser($this->uid);
+ $notificationManager->markProcessed($notification);
+
+ $accountManager = \OCP\Server::get(AccountManager::class);
+ $accountManager->deleteUser($this);
+
+ $database = \OCP\Server::get(IDBConnection::class);
+ try {
+ // We need to create a transaction to make sure we are in a defined state
+ // because if all user values are removed also the flag is gone, but if an exception happens (e.g. database lost connection on the set operation)
+ // exactly here we are in an undefined state as the data is still present but the user does not exist on the system anymore.
+ $database->beginTransaction();
+ // Remove all user settings
+ $this->config->deleteAllUserValues($this->uid);
+ // But again set flag that this user is about to be deleted
+ $this->config->setUserValue($this->uid, 'core', 'deleted', 'true');
+ $this->config->setUserValue($this->uid, 'core', 'deleted.home-path', $this->getHome());
+ // Commit the transaction so we are in a defined state: either the preferences are removed or an exception occurred but the delete flag is still present
+ $database->commit();
+ } catch (\Throwable $e) {
+ $database->rollback();
+ throw $e;
+ }
+ if ($this->emitter !== null) {
/** @deprecated 21.0.0 use UserDeletedEvent event with the IEventDispatcher instead */
- $this->legacyDispatcher->dispatch(IUser::class . '::postDelete', new GenericEvent($this));
- if ($this->emitter) {
- /** @deprecated 21.0.0 use UserDeletedEvent event with the IEventDispatcher instead */
- $this->emitter->emit('\OC\User', 'postDelete', [$this]);
- }
- $this->dispatcher->dispatchTyped(new UserDeletedEvent($this));
+ $this->emitter->emit('\OC\User', 'postDelete', [$this]);
}
- return !($result === false);
+ $this->dispatcher->dispatchTyped(new UserDeletedEvent($this));
+
+ // Finally we can unset the delete flag and all other states
+ $this->config->deleteAllUserValues($this->uid);
+
+ return true;
}
/**
@@ -324,10 +344,7 @@ class User implements IUser {
* @return bool
*/
public function setPassword($password, $recoveryPassword = null) {
- $this->legacyDispatcher->dispatch(IUser::class . '::preSetPassword', new GenericEvent($this, [
- 'password' => $password,
- 'recoveryPassword' => $recoveryPassword,
- ]));
+ $this->dispatcher->dispatchTyped(new BeforePasswordUpdatedEvent($this, $password, $recoveryPassword));
if ($this->emitter) {
$this->emitter->emit('\OC\User', 'preSetPassword', [$this, $password, $recoveryPassword]);
}
@@ -337,10 +354,7 @@ class User implements IUser {
$result = $backend->setPassword($this->uid, $password);
if ($result !== false) {
- $this->legacyDispatcher->dispatch(IUser::class . '::postSetPassword', new GenericEvent($this, [
- 'password' => $password,
- 'recoveryPassword' => $recoveryPassword,
- ]));
+ $this->dispatcher->dispatchTyped(new PasswordUpdatedEvent($this, $password, $recoveryPassword));
if ($this->emitter) {
$this->emitter->emit('\OC\User', 'postSetPassword', [$this, $password, $recoveryPassword]);
}
@@ -352,6 +366,20 @@ class User implements IUser {
}
}
+ public function getPasswordHash(): ?string {
+ if (!($this->backend instanceof IPasswordHashBackend)) {
+ return null;
+ }
+ return $this->backend->getPasswordHash($this->uid);
+ }
+
+ public function setPasswordHash(string $passwordHash): bool {
+ if (!($this->backend instanceof IPasswordHashBackend)) {
+ return false;
+ }
+ return $this->backend->setPasswordHash($this->uid, $passwordHash);
+ }
+
/**
* get the users home folder to mount
*
@@ -362,10 +390,8 @@ class User implements IUser {
/** @psalm-suppress UndefinedInterfaceMethod Once we get rid of the legacy implementsActions, psalm won't complain anymore */
if (($this->backend instanceof IGetHomeBackend || $this->backend->implementsActions(Backend::GET_HOME)) && $home = $this->backend->getHome($this->uid)) {
$this->home = $home;
- } elseif ($this->config) {
- $this->home = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $this->uid;
} else {
- $this->home = \OC::$SERVERROOT . '/data/' . $this->uid;
+ $this->home = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $this->uid;
}
}
return $this->home;
@@ -388,7 +414,7 @@ class User implements IUser {
}
/**
- * Check if the backend allows the user to change his avatar on Personal page
+ * Check if the backend allows the user to change their avatar on Personal page
*
* @return bool
*/
@@ -416,37 +442,63 @@ class User implements IUser {
* @return bool
*/
public function canChangeDisplayName() {
- if ($this->config->getSystemValue('allow_user_to_change_display_name') === false) {
+ if (!$this->config->getSystemValueBool('allow_user_to_change_display_name', true)) {
return false;
}
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
*
* @return bool
*/
public function isEnabled() {
- if ($this->enabled === null) {
- $enabled = $this->config->getUserValue($this->uid, 'core', 'enabled', 'true');
- $this->enabled = $enabled === 'true';
+ $queryDatabaseValue = function (): bool {
+ if ($this->enabled === null) {
+ $enabled = $this->config->getUserValue($this->uid, 'core', 'enabled', 'true');
+ $this->enabled = $enabled === 'true';
+ }
+ return $this->enabled;
+ };
+ if ($this->backend instanceof IProvideEnabledStateBackend) {
+ return $this->backend->isUserEnabled($this->uid, $queryDatabaseValue);
+ } else {
+ return $queryDatabaseValue();
}
- return (bool) $this->enabled;
}
/**
* set the enabled status for the user
*
- * @param bool $enabled
+ * @return void
*/
public function setEnabled(bool $enabled = true) {
$oldStatus = $this->isEnabled();
- $this->enabled = $enabled;
- if ($oldStatus !== $this->enabled) {
- // TODO: First change the value, then trigger the event as done for all other properties.
- $this->triggerChange('enabled', $enabled, $oldStatus);
+ $setDatabaseValue = function (bool $enabled): void {
$this->config->setUserValue($this->uid, 'core', 'enabled', $enabled ? 'true' : 'false');
+ $this->enabled = $enabled;
+ };
+ if ($this->backend instanceof IProvideEnabledStateBackend) {
+ $queryDatabaseValue = function (): bool {
+ if ($this->enabled === null) {
+ $enabled = $this->config->getUserValue($this->uid, 'core', 'enabled', 'true');
+ $this->enabled = $enabled === 'true';
+ }
+ return $this->enabled;
+ };
+ $enabled = $this->backend->setUserEnabled($this->uid, $enabled, $queryDatabaseValue, $setDatabaseValue);
+ if ($oldStatus !== $enabled) {
+ $this->triggerChange('enabled', $enabled, $oldStatus);
+ }
+ } elseif ($oldStatus !== $enabled) {
+ $setDatabaseValue($enabled);
+ $this->triggerChange('enabled', $enabled, $oldStatus);
}
}
@@ -464,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;
}
/**
@@ -508,23 +562,62 @@ 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
*
* @param string $quota
* @return void
+ * @throws InvalidArgumentException
* @since 9.0.0
*/
public function setQuota($quota) {
$oldQuota = $this->config->getUserValue($this->uid, 'files', 'quota', '');
if ($quota !== 'none' and $quota !== 'default') {
- $quota = OC_Helper::computerFileSize($quota);
- $quota = OC_Helper::humanFileSize((int)$quota);
+ $bytesQuota = \OCP\Util::computerFileSize($quota);
+ if ($bytesQuota === false) {
+ throw new InvalidArgumentException('Failed to set quota to invalid value ' . $quota);
+ }
+ $quota = \OCP\Util::humanFileSize($bytesQuota);
}
if ($quota !== $oldQuota) {
$this->config->setUserValue($this->uid, 'files', 'quota', $quota);
$this->triggerChange('quota', $quota, $oldQuota);
}
+ \OC_Helper::clearStorageInfo('/' . $this->uid . '/files');
+ }
+
+ public function getManagerUids(): array {
+ $encodedUids = $this->config->getUserValue(
+ $this->uid,
+ 'settings',
+ self::CONFIG_KEY_MANAGERS,
+ '[]'
+ );
+ return json_decode($encodedUids, false, 512, JSON_THROW_ON_ERROR);
+ }
+
+ public function setManagerUids(array $uids): void {
+ $oldUids = $this->getManagerUids();
+ $this->config->setUserValue(
+ $this->uid,
+ 'settings',
+ self::CONFIG_KEY_MANAGERS,
+ json_encode($uids, JSON_THROW_ON_ERROR)
+ );
+ $this->triggerChange('managers', $uids, $oldUids);
}
/**
@@ -537,11 +630,11 @@ class User implements IUser {
public function getAvatarImage($size) {
// delay the initialization
if (is_null($this->avatarManager)) {
- $this->avatarManager = \OC::$server->getAvatarManager();
+ $this->avatarManager = \OC::$server->get(IAvatarManager::class);
}
$avatar = $this->avatarManager->getAvatar($this->uid);
- $image = $avatar->get(-1);
+ $image = $avatar->get($size);
if ($image) {
return $image;
}
@@ -558,7 +651,7 @@ class User implements IUser {
public function getCloudId() {
$uid = $this->getUID();
$server = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/');
- if (substr($server, -10) === '/index.php') {
+ if (str_ends_with($server, '/index.php')) {
$server = substr($server, 0, -10);
}
$server = $this->removeProtocolFromUrl($server);
@@ -566,7 +659,7 @@ class User implements IUser {
}
private function removeProtocolFromUrl(string $url): string {
- if (strpos($url, 'https://') === 0) {
+ if (str_starts_with($url, 'https://')) {
return substr($url, strlen('https://'));
}
@@ -574,11 +667,7 @@ class User implements IUser {
}
public function triggerChange($feature, $value = null, $oldValue = null) {
- $this->legacyDispatcher->dispatch(IUser::class . '::changeUser', new GenericEvent($this, [
- 'feature' => $feature,
- 'value' => $value,
- 'oldValue' => $oldValue,
- ]));
+ $this->dispatcher->dispatchTyped(new UserChangedEvent($this, $feature, $value, $oldValue));
if ($this->emitter) {
$this->emitter->emit('\OC\User', 'changeUser', [$this, $feature, $value, $oldValue]);
}