aboutsummaryrefslogtreecommitdiffstats
path: root/core/Command/User
diff options
context:
space:
mode:
Diffstat (limited to 'core/Command/User')
-rw-r--r--core/Command/User/Add.php132
-rw-r--r--core/Command/User/AddAppPassword.php137
-rw-r--r--core/Command/User/AuthTokens/Add.php104
-rw-r--r--core/Command/User/AuthTokens/Delete.php104
-rw-r--r--core/Command/User/AuthTokens/ListCommand.php84
-rw-r--r--core/Command/User/ClearGeneratedAvatarCacheCommand.php35
-rw-r--r--core/Command/User/Delete.php56
-rw-r--r--core/Command/User/Disable.php60
-rw-r--r--core/Command/User/Enable.php60
-rw-r--r--core/Command/User/Info.php93
-rw-r--r--core/Command/User/Keys/Verify.php83
-rw-r--r--core/Command/User/LastSeen.php117
-rw-r--r--core/Command/User/ListCommand.php94
-rw-r--r--core/Command/User/Profile.php234
-rw-r--r--core/Command/User/Report.php79
-rw-r--r--core/Command/User/ResetPassword.php79
-rw-r--r--core/Command/User/Setting.php121
-rw-r--r--core/Command/User/SyncAccountDataCommand.php84
-rw-r--r--core/Command/User/Welcome.php78
19 files changed, 1256 insertions, 578 deletions
diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php
index b75664f16f6..4de4e247991 100644
--- a/core/Command/User/Add.php
+++ b/core/Command/User/Add.php
@@ -1,36 +1,23 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Laurens Post <lkpost@scept.re>
- * @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\Core\Command\User;
use OC\Files\Filesystem;
+use OCA\Settings\Mailer\NewUserMailHelper;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IAppConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
+use OCP\Mail\IMailer;
+use OCP\Security\Events\GenerateSecurePasswordEvent;
+use OCP\Security\ISecureRandom;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
@@ -40,64 +27,80 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class Add extends Command {
- /** @var \OCP\IUserManager */
- protected $userManager;
-
- /** @var \OCP\IGroupManager */
- protected $groupManager;
-
- /**
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- */
- public function __construct(IUserManager $userManager, IGroupManager $groupManager) {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ protected IMailer $mailer,
+ private IAppConfig $appConfig,
+ private NewUserMailHelper $mailHelper,
+ private IEventDispatcher $eventDispatcher,
+ private ISecureRandom $secureRandom,
+ ) {
parent::__construct();
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('user:add')
- ->setDescription('adds a user')
+ ->setDescription('adds an account')
->addArgument(
'uid',
InputArgument::REQUIRED,
- 'User ID used to login (must only contain a-z, A-Z, 0-9, -, _ and @)'
+ 'Account ID used to login (must only contain a-z, A-Z, 0-9, -, _ and @)'
)
->addOption(
'password-from-env',
null,
InputOption::VALUE_NONE,
- 'read password from environment variable OC_PASS'
+ 'read password from environment variable NC_PASS/OC_PASS'
+ )
+ ->addOption(
+ 'generate-password',
+ null,
+ InputOption::VALUE_NONE,
+ 'Generate a secure password. A welcome email with a reset link will be sent to the user via an email if --email option and newUser.sendEmail config are set'
)
->addOption(
'display-name',
null,
InputOption::VALUE_OPTIONAL,
- 'User name used in the web UI (can contain any characters)'
+ 'Login used in the web UI (can contain any characters)'
)
->addOption(
'group',
'g',
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
- 'groups the user should be added to (The group will be created if it does not exist)'
+ 'groups the account should be added to (The group will be created if it does not exist)'
+ )
+ ->addOption(
+ 'email',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'When set, users may register using the default email verification workflow'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$uid = $input->getArgument('uid');
if ($this->userManager->userExists($uid)) {
- $output->writeln('<error>The user "' . $uid . '" already exists.</error>');
+ $output->writeln('<error>The account "' . $uid . '" already exists.</error>');
return 1;
}
+ $password = '';
+
+ // Setup password.
if ($input->getOption('password-from-env')) {
- $password = getenv('OC_PASS');
+ $password = getenv('NC_PASS') ?: getenv('OC_PASS');
+
if (!$password) {
- $output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>');
+ $output->writeln('<error>--password-from-env given, but NC_PASS/OC_PASS is empty!</error>');
return 1;
}
+ } elseif ($input->getOption('generate-password')) {
+ $passwordEvent = new GenerateSecurePasswordEvent();
+ $this->eventDispatcher->dispatchTyped($passwordEvent);
+ $password = $passwordEvent->getPassword() ?? $this->secureRandom->generate(20);
} elseif ($input->isInteractive()) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
@@ -108,32 +111,31 @@ class Add extends Command {
$question = new Question('Confirm password: ');
$question->setHidden(true);
- $confirm = $helper->ask($input, $output,$question);
+ $confirm = $helper->ask($input, $output, $question);
if ($password !== $confirm) {
- $output->writeln("<error>Passwords did not match!</error>");
+ $output->writeln('<error>Passwords did not match!</error>');
return 1;
}
} else {
- $output->writeln("<error>Interactive input or --password-from-env is needed for entering a password!</error>");
+ $output->writeln('<error>Interactive input or --password-from-env or --generate-password is needed for setting a password!</error>');
return 1;
}
try {
$user = $this->userManager->createUser(
$input->getArgument('uid'),
- $password
+ $password,
);
} catch (\Exception $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
return 1;
}
-
if ($user instanceof IUser) {
- $output->writeln('<info>The user "' . $user->getUID() . '" was created successfully</info>');
+ $output->writeln('<info>The account "' . $user->getUID() . '" was created successfully</info>');
} else {
- $output->writeln('<error>An error occurred while creating the user</error>');
+ $output->writeln('<error>An error occurred while creating the account</error>');
return 1;
}
@@ -161,9 +163,33 @@ class Add extends Command {
}
if ($group instanceof IGroup) {
$group->addUser($user);
- $output->writeln('User "' . $user->getUID() . '" added to group "' . $group->getGID() . '"');
+ $output->writeln('Account "' . $user->getUID() . '" added to group "' . $group->getGID() . '"');
+ }
+ }
+
+ $email = $input->getOption('email');
+ if (!empty($email)) {
+ if (!$this->mailer->validateMailAddress($email)) {
+ $output->writeln(\sprintf(
+ '<error>The given email address "%s" is invalid. Email not set for the user.</error>',
+ $email,
+ ));
+
+ return 1;
+ }
+
+ $user->setSystemEMailAddress($email);
+
+ if ($this->appConfig->getValueString('core', 'newUser.sendEmail', 'yes') === 'yes') {
+ try {
+ $this->mailHelper->sendMail($user, $this->mailHelper->generateTemplate($user, true));
+ $output->writeln('Welcome email sent to ' . $email);
+ } catch (\Exception $e) {
+ $output->writeln('Unable to send the welcome email to ' . $email);
+ }
}
}
+
return 0;
}
}
diff --git a/core/Command/User/AddAppPassword.php b/core/Command/User/AddAppPassword.php
deleted file mode 100644
index d7011342030..00000000000
--- a/core/Command/User/AddAppPassword.php
+++ /dev/null
@@ -1,137 +0,0 @@
-<?php
-/**
- * @copyright Copyright (c) 2020, NextCloud, Inc.
- *
- * @author Andreas Fischer <bantu@owncloud.com>
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Clark Tomlinson <fallen013@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Laurens Post <lkpost@scept.re>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sujith H <sharidasan@owncloud.com>
- * @author Sean Molenaar <sean@seanmolenaar.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/>
- *
- */
-
-namespace OC\Core\Command\User;
-
-use OC\Authentication\Token\IProvider;
-use OC\Authentication\Token\IToken;
-use OCP\IUserManager;
-use OCP\Security\ICrypto;
-use OCP\Security\ISecureRandom;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Helper\QuestionHelper;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\Question;
-
-class AddAppPassword extends Command {
-
- /** @var IUserManager */
- protected $userManager;
- /** @var IProvider */
- protected $tokenProvider;
- /** @var ISecureRandom */
- private $random;
- /** @var ICrypto */
- private $crypto;
-
- public function __construct(IUserManager $userManager,
- IProvider $tokenProvider,
- ISecureRandom $random,
- ICrypto $crypto) {
- $this->tokenProvider = $tokenProvider;
- $this->userManager = $userManager;
- $this->random = $random;
- $this->crypto = $crypto;
- parent::__construct();
- }
-
- protected function configure() {
- $this
- ->setName('user:add-app-password')
- ->setDescription('Add app password for the named user')
- ->addArgument(
- 'user',
- InputArgument::REQUIRED,
- 'Username to add app password for'
- )
- ->addOption(
- 'password-from-env',
- null,
- InputOption::VALUE_NONE,
- 'read password from environment variable NC_PASS/OC_PASS'
- )
- ;
- }
-
- protected function execute(InputInterface $input, OutputInterface $output): int {
- $username = $input->getArgument('user');
-
- $user = $this->userManager->get($username);
- if (is_null($user)) {
- $output->writeln('<error>User does not exist</error>');
- return 1;
- }
-
- if ($input->getOption('password-from-env')) {
- $password = getenv('NC_PASS') ?? getenv('OC_PASS');
- if (!$password) {
- $output->writeln('<error>--password-from-env given, but NC_PASS is empty!</error>');
- return 1;
- }
- } elseif ($input->isInteractive()) {
- /** @var QuestionHelper $helper */
- $helper = $this->getHelper('question');
-
- $question = new Question('Enter the user password: ');
- $question->setHidden(true);
- $password = $helper->ask($input, $output, $question);
-
- if ($password === null) {
- $output->writeln("<error>Password cannot be empty!</error>");
- return 1;
- }
- } else {
- $output->writeln("<error>Interactive input or --password-from-env is needed for entering a new password!</error>");
- return 1;
- }
-
- $output->writeln('<info>The password is not validated so what you provide is what gets recorded in the token</info>');
-
-
- $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
- $this->tokenProvider->generateToken(
- $token,
- $user->getUID(),
- $user->getDisplayName(),
- $password,
- 'cli',
- IToken::PERMANENT_TOKEN,
- IToken::DO_NOT_REMEMBER
- );
-
- $output->writeln('app password:');
- $output->writeln($token);
-
- return 0;
- }
-}
diff --git a/core/Command/User/AuthTokens/Add.php b/core/Command/User/AuthTokens/Add.php
new file mode 100644
index 00000000000..89b20535c63
--- /dev/null
+++ b/core/Command/User/AuthTokens/Add.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\User\AuthTokens;
+
+use OC\Authentication\Events\AppPasswordCreatedEvent;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUserManager;
+use OCP\Security\ISecureRandom;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\Question;
+
+class Add extends Command {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IProvider $tokenProvider,
+ private ISecureRandom $random,
+ private IEventDispatcher $eventDispatcher,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('user:auth-tokens:add')
+ ->setAliases(['user:add-app-password'])
+ ->setDescription('Add app password for the named account')
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'Login to add app password for'
+ )
+ ->addOption(
+ 'password-from-env',
+ null,
+ InputOption::VALUE_NONE,
+ 'Read password from environment variable NC_PASS/OC_PASS. Alternatively it will be asked for interactively or an app password without the login password will be created.'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $username = $input->getArgument('user');
+ $password = null;
+
+ $user = $this->userManager->get($username);
+ if (is_null($user)) {
+ $output->writeln('<error>Account does not exist</error>');
+ return 1;
+ }
+
+ if ($input->getOption('password-from-env')) {
+ $password = getenv('NC_PASS') ?: getenv('OC_PASS');
+ if (!$password) {
+ $output->writeln('<error>--password-from-env given, but NC_PASS/OC_PASS is empty!</error>');
+ return 1;
+ }
+ } elseif ($input->isInteractive()) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+
+ $question = new Question('Enter the account password: ');
+ $question->setHidden(true);
+ /** @var null|string $password */
+ $password = $helper->ask($input, $output, $question);
+ }
+
+ if ($password === null) {
+ $output->writeln('<info>No password provided. The generated app password will therefore have limited capabilities. Any operation that requires the login password will fail.</info>');
+ }
+
+ $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
+ $generatedToken = $this->tokenProvider->generateToken(
+ $token,
+ $user->getUID(),
+ $user->getUID(),
+ $password,
+ 'cli',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+
+ $this->eventDispatcher->dispatchTyped(
+ new AppPasswordCreatedEvent($generatedToken)
+ );
+
+ $output->writeln('app password:');
+ $output->writeln($token);
+
+ return 0;
+ }
+}
diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php
new file mode 100644
index 00000000000..2047d2eae2a
--- /dev/null
+++ b/core/Command/User/AuthTokens/Delete.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\User\AuthTokens;
+
+use DateTimeImmutable;
+use OC\Authentication\Token\IProvider;
+use OC\Core\Command\Base;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\RuntimeException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Delete extends Base {
+ public function __construct(
+ protected IProvider $tokenProvider,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('user:auth-tokens:delete')
+ ->setDescription('Deletes an authentication token')
+ ->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'ID of the user to delete tokens for'
+ )
+ ->addArgument(
+ 'id',
+ InputArgument::OPTIONAL,
+ 'ID of the auth token to delete'
+ )
+ ->addOption(
+ 'last-used-before',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'Delete tokens last used before a given date.'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $uid = $input->getArgument('uid');
+ $id = (int)$input->getArgument('id');
+ $before = $input->getOption('last-used-before');
+
+ if ($before) {
+ if ($id) {
+ throw new RuntimeException('Option --last-used-before cannot be used with [<id>]');
+ }
+
+ return $this->deleteLastUsedBefore($uid, $before);
+ }
+
+ if (!$id) {
+ throw new RuntimeException('Not enough arguments. Specify the token <id> or use the --last-used-before option.');
+ }
+ return $this->deleteById($uid, $id);
+ }
+
+ protected function deleteById(string $uid, int $id): int {
+ $this->tokenProvider->invalidateTokenById($uid, $id);
+
+ return Command::SUCCESS;
+ }
+
+ protected function deleteLastUsedBefore(string $uid, string $before): int {
+ $date = $this->parseDateOption($before);
+ if (!$date) {
+ throw new RuntimeException('Invalid date format. Acceptable formats are: ISO8601 (w/o fractions), "YYYY-MM-DD" and Unix time in seconds.');
+ }
+
+ $this->tokenProvider->invalidateLastUsedBefore($uid, $date->getTimestamp());
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * @return \DateTimeImmutable|false
+ */
+ protected function parseDateOption(string $input) {
+ $date = false;
+
+ // Handle Unix timestamp
+ if (filter_var($input, FILTER_VALIDATE_INT)) {
+ return new DateTimeImmutable('@' . $input);
+ }
+
+ // ISO8601
+ $date = DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $input);
+ if ($date) {
+ return $date;
+ }
+
+ // YYYY-MM-DD
+ return DateTimeImmutable::createFromFormat('!Y-m-d', $input);
+ }
+}
diff --git a/core/Command/User/AuthTokens/ListCommand.php b/core/Command/User/AuthTokens/ListCommand.php
new file mode 100644
index 00000000000..b36aa717505
--- /dev/null
+++ b/core/Command/User/AuthTokens/ListCommand.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\User\AuthTokens;
+
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Core\Command\Base;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListCommand extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IProvider $tokenProvider,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName('user:auth-tokens:list')
+ ->setDescription('List authentication tokens of an user')
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'User to list auth tokens for'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = $this->userManager->get($input->getArgument('user'));
+
+ if (is_null($user)) {
+ $output->writeln('<error>user not found</error>');
+ return 1;
+ }
+
+ $tokens = $this->tokenProvider->getTokenByUser($user->getUID());
+
+ $tokens = array_map(function (IToken $token) use ($input): mixed {
+ $sensitive = [
+ 'password',
+ 'password_hash',
+ 'token',
+ 'public_key',
+ 'private_key',
+ ];
+ $data = array_diff_key($token->jsonSerialize(), array_flip($sensitive));
+
+ if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) {
+ $data = $this->formatTokenForPlainOutput($data);
+ }
+
+ return $data;
+ }, $tokens);
+
+ $this->writeTableInOutputFormat($input, $output, $tokens);
+
+ return 0;
+ }
+
+ public function formatTokenForPlainOutput(array $token): array {
+ $token['scope'] = implode(', ', array_keys(array_filter($token['scope'] ?? [])));
+
+ $token['lastActivity'] = date(DATE_ATOM, $token['lastActivity']);
+
+ $token['type'] = match ($token['type']) {
+ IToken::TEMPORARY_TOKEN => 'temporary',
+ IToken::PERMANENT_TOKEN => 'permanent',
+ IToken::WIPE_TOKEN => 'wipe',
+ default => $token['type'],
+ };
+
+ return $token;
+ }
+}
diff --git a/core/Command/User/ClearGeneratedAvatarCacheCommand.php b/core/Command/User/ClearGeneratedAvatarCacheCommand.php
new file mode 100644
index 00000000000..515b3a913b7
--- /dev/null
+++ b/core/Command/User/ClearGeneratedAvatarCacheCommand.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\User;
+
+use OC\Avatar\AvatarManager;
+use OC\Core\Command\Base;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ClearGeneratedAvatarCacheCommand extends Base {
+ public function __construct(
+ protected AvatarManager $avatarManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setDescription('clear avatar cache')
+ ->setName('user:clear-avatar-cache');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $output->writeln('Clearing avatar cache has started');
+ $this->avatarManager->clearCachedAvatars();
+ $output->writeln('Cleared avatar cache successfully');
+ return 0;
+ }
+}
diff --git a/core/Command/User/Delete.php b/core/Command/User/Delete.php
index c6c7700ce5a..c5d0578f5f8 100644
--- a/core/Command/User/Delete.php
+++ b/core/Command/User/Delete.php
@@ -1,46 +1,24 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Jens-Christian Fischer <jens-christian.fischer@switch.ch>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.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\Core\Command\User;
+use OC\Core\Command\Base;
+use OCP\IUser;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class Delete extends Command {
- /** @var IUserManager */
- protected $userManager;
-
- /**
- * @param IUserManager $userManager
- */
- public function __construct(IUserManager $userManager) {
- $this->userManager = $userManager;
+class Delete extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ ) {
parent::__construct();
}
@@ -70,4 +48,16 @@ class Delete extends Command {
$output->writeln('<error>The specified user could not be deleted. Please check the logs.</error>');
return 1;
}
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'uid') {
+ return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord()));
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/Disable.php b/core/Command/User/Disable.php
index ac751f5da9f..4713950bf30 100644
--- a/core/Command/User/Disable.php
+++ b/core/Command/User/Disable.php
@@ -1,44 +1,24 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @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\Core\Command\User;
+use OC\Core\Command\Base;
+use OCP\IUser;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class Disable extends Command {
- /** @var IUserManager */
- protected $userManager;
-
- /**
- * @param IUserManager $userManager
- */
- public function __construct(IUserManager $userManager) {
- $this->userManager = $userManager;
+class Disable extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ ) {
parent::__construct();
}
@@ -64,4 +44,22 @@ class Disable extends Command {
$output->writeln('<info>The specified user is disabled</info>');
return 0;
}
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'uid') {
+ return array_map(
+ static fn (IUser $user) => $user->getUID(),
+ array_filter(
+ $this->userManager->search($context->getCurrentWord()),
+ static fn (IUser $user) => $user->isEnabled()
+ )
+ );
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/Enable.php b/core/Command/User/Enable.php
index e556821061d..23f56e5dd4f 100644
--- a/core/Command/User/Enable.php
+++ b/core/Command/User/Enable.php
@@ -1,44 +1,24 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @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\Core\Command\User;
+use OC\Core\Command\Base;
+use OCP\IUser;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-class Enable extends Command {
- /** @var IUserManager */
- protected $userManager;
-
- /**
- * @param IUserManager $userManager
- */
- public function __construct(IUserManager $userManager) {
- $this->userManager = $userManager;
+class Enable extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ ) {
parent::__construct();
}
@@ -64,4 +44,22 @@ class Enable extends Command {
$output->writeln('<info>The specified user is enabled</info>');
return 0;
}
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'uid') {
+ return array_map(
+ static fn (IUser $user) => $user->getUID(),
+ array_filter(
+ $this->userManager->search($context->getCurrentWord()),
+ static fn (IUser $user) => !$user->isEnabled()
+ )
+ );
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/Info.php b/core/Command/User/Info.php
index 4727ef0ec9a..e7fc9286e74 100644
--- a/core/Command/User/Info.php
+++ b/core/Command/User/Info.php
@@ -1,51 +1,27 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OC\Core\Command\User;
use OC\Core\Command\Base;
+use OCP\Files\NotFoundException;
use OCP\IGroupManager;
+use OCP\IUser;
use OCP\IUserManager;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Info extends Base {
- /** @var IUserManager */
- protected $userManager;
- /** @var IGroupManager */
- protected $groupManager;
-
- /**
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- */
- public function __construct(IUserManager $userManager, IGroupManager $groupManager) {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ ) {
parent::__construct();
}
@@ -76,16 +52,61 @@ class Info extends Base {
$data = [
'user_id' => $user->getUID(),
'display_name' => $user->getDisplayName(),
- 'email' => $user->getEMailAddress() ? $user->getEMailAddress() : '',
+ 'email' => (string)$user->getSystemEMailAddress(),
'cloud_id' => $user->getCloudId(),
'enabled' => $user->isEnabled(),
'groups' => $groups,
'quota' => $user->getQuota(),
- 'last_seen' => date(\DateTime::ATOM, $user->getLastLogin()), // ISO-8601
+ 'storage' => $this->getStorageInfo($user),
+ 'first_seen' => $this->formatLoginDate($user->getFirstLogin()),
+ 'last_seen' => $this->formatLoginDate($user->getLastLogin()),
'user_directory' => $user->getHome(),
'backend' => $user->getBackendClassName()
];
$this->writeArrayInOutputFormat($input, $output, $data);
return 0;
}
+
+ private function formatLoginDate(int $timestamp): string {
+ if ($timestamp < 0) {
+ return 'unknown';
+ } elseif ($timestamp === 0) {
+ return 'never';
+ } else {
+ return date(\DateTimeInterface::ATOM, $timestamp); // ISO-8601
+ }
+ }
+
+ /**
+ * @param IUser $user
+ * @return array
+ */
+ protected function getStorageInfo(IUser $user): array {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($user->getUID());
+ try {
+ $storage = \OC_Helper::getStorageInfo('/');
+ } catch (NotFoundException $e) {
+ return [];
+ }
+ return [
+ 'free' => $storage['free'],
+ 'used' => $storage['used'],
+ 'total' => $storage['total'],
+ 'relative' => $storage['relative'],
+ 'quota' => $storage['quota'],
+ ];
+ }
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'user') {
+ return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord()));
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/Keys/Verify.php b/core/Command/User/Keys/Verify.php
new file mode 100644
index 00000000000..024e9346072
--- /dev/null
+++ b/core/Command/User/Keys/Verify.php
@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Command\User\Keys;
+
+use OC\Security\IdentityProof\Manager;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Verify extends Command {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected Manager $keyManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('user:keys:verify')
+ ->setDescription('Verify if the stored public key matches the stored private key')
+ ->addArgument(
+ 'user-id',
+ InputArgument::REQUIRED,
+ 'User ID of the user to verify'
+ )
+ ;
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('user-id');
+
+ $user = $this->userManager->get($userId);
+ if (!$user instanceof IUser) {
+ $output->writeln('Unknown user');
+ return static::FAILURE;
+ }
+
+ $key = $this->keyManager->getKey($user);
+ $publicKey = $key->getPublic();
+ $privateKey = $key->getPrivate();
+
+ $output->writeln('User public key size: ' . strlen($publicKey));
+ $output->writeln('User private key size: ' . strlen($privateKey));
+
+ // Derive the public key from the private key again to validate the stored public key
+ $opensslPrivateKey = openssl_pkey_get_private($privateKey);
+ $publicKeyDerived = openssl_pkey_get_details($opensslPrivateKey);
+ $publicKeyDerived = $publicKeyDerived['key'];
+ $output->writeln('User derived public key size: ' . strlen($publicKeyDerived));
+
+ $output->writeln('');
+
+ $output->writeln('Stored public key:');
+ $output->writeln($publicKey);
+ $output->writeln('Derived public key:');
+ $output->writeln($publicKeyDerived);
+
+ if ($publicKey != $publicKeyDerived) {
+ $output->writeln('<error>Stored public key does not match stored private key</error>');
+ return static::FAILURE;
+ }
+
+ $output->writeln('<info>Stored public key matches stored private key</info>');
+
+ return static::SUCCESS;
+ }
+}
diff --git a/core/Command/User/LastSeen.php b/core/Command/User/LastSeen.php
index af80b1e1c05..984def72cd6 100644
--- a/core/Command/User/LastSeen.php
+++ b/core/Command/User/LastSeen.php
@@ -1,78 +1,95 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Pierre Ozoux <pierre@ozoux.net>
- * @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\Core\Command\User;
+use OC\Core\Command\Base;
+use OCP\IUser;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-class LastSeen extends Command {
- /** @var IUserManager */
- protected $userManager;
-
- /**
- * @param IUserManager $userManager
- */
- public function __construct(IUserManager $userManager) {
- $this->userManager = $userManager;
+class LastSeen extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ ) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('user:lastseen')
->setDescription('shows when the user was logged in last time')
->addArgument(
'uid',
- InputArgument::REQUIRED,
+ InputArgument::OPTIONAL,
'the username'
- );
+ )
+ ->addOption(
+ 'all',
+ null,
+ InputOption::VALUE_NONE,
+ 'shows a list of when all users were last logged in'
+ )
+ ;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
- $user = $this->userManager->get($input->getArgument('uid'));
- if (is_null($user)) {
- $output->writeln('<error>User does not exist</error>');
- return 1;
+ $singleUserId = $input->getArgument('uid');
+
+ if ($singleUserId) {
+ $user = $this->userManager->get($singleUserId);
+ if (is_null($user)) {
+ $output->writeln('<error>User does not exist</error>');
+ return 1;
+ }
+
+ $lastLogin = $user->getLastLogin();
+ if ($lastLogin === 0) {
+ $output->writeln($user->getUID() . ' has never logged in.');
+ } else {
+ $date = new \DateTime();
+ $date->setTimestamp($lastLogin);
+ $output->writeln($user->getUID() . "'s last login: " . $date->format('Y-m-d H:i:s T'));
+ }
+
+ return 0;
}
- $lastLogin = $user->getLastLogin();
- if ($lastLogin === 0) {
- $output->writeln('User ' . $user->getUID() .
- ' has never logged in, yet.');
- } else {
- $date = new \DateTime();
- $date->setTimestamp($lastLogin);
- $output->writeln($user->getUID() .
- '`s last login: ' . $date->format('d.m.Y H:i'));
+ if (!$input->getOption('all')) {
+ $output->writeln('<error>Please specify a username, or "--all" to list all</error>');
+ return 1;
}
+
+ $this->userManager->callForAllUsers(static function (IUser $user) use ($output): void {
+ $lastLogin = $user->getLastLogin();
+ if ($lastLogin === 0) {
+ $output->writeln($user->getUID() . ' has never logged in.');
+ } else {
+ $date = new \DateTime();
+ $date->setTimestamp($lastLogin);
+ $output->writeln($user->getUID() . "'s last login: " . $date->format('Y-m-d H:i:s T'));
+ }
+ });
return 0;
}
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'uid') {
+ return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord()));
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/ListCommand.php b/core/Command/User/ListCommand.php
index 6c153ee8da7..66b831c793b 100644
--- a/core/Command/User/ListCommand.php
+++ b/core/Command/User/ListCommand.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
- * @author 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: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OC\Core\Command\User;
use OC\Core\Command\Base;
@@ -34,21 +15,10 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ListCommand extends Base {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /**
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- */
- public function __construct(IUserManager $userManager,
- IGroupManager $groupManager) {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ ) {
parent::__construct();
}
@@ -57,17 +27,22 @@ class ListCommand extends Base {
->setName('user:list')
->setDescription('list configured users')
->addOption(
+ 'disabled',
+ 'd',
+ InputOption::VALUE_NONE,
+ 'List disabled users only'
+ )->addOption(
'limit',
'l',
InputOption::VALUE_OPTIONAL,
'Number of users to retrieve',
- 500
+ '500'
)->addOption(
'offset',
'o',
InputOption::VALUE_OPTIONAL,
'Offset for retrieving users',
- 0
+ '0'
)->addOption(
'output',
null,
@@ -83,7 +58,11 @@ class ListCommand extends Base {
}
protected function execute(InputInterface $input, OutputInterface $output): int {
- $users = $this->userManager->search('', (int) $input->getOption('limit'), (int) $input->getOption('offset'));
+ if ($input->getOption('disabled')) {
+ $users = $this->userManager->getDisabledUsers((int)$input->getOption('limit'), (int)$input->getOption('offset'));
+ } else {
+ $users = $this->userManager->searchDisplayName('', (int)$input->getOption('limit'), (int)$input->getOption('offset'));
+ }
$this->writeArrayInOutputFormat($input, $output, $this->formatUsers($users, (bool)$input->getOption('info')));
return 0;
@@ -91,32 +70,39 @@ class ListCommand extends Base {
/**
* @param IUser[] $users
- * @param bool [$detailed=false]
- * @return array
+ * @return \Generator<string,string|array>
*/
- private function formatUsers(array $users, bool $detailed = false) {
- $keys = array_map(function (IUser $user) {
- return $user->getUID();
- }, $users);
-
- $values = array_map(function (IUser $user) use ($detailed) {
+ private function formatUsers(array $users, bool $detailed = false): \Generator {
+ foreach ($users as $user) {
if ($detailed) {
$groups = $this->groupManager->getUserGroupIds($user);
- return [
+ $value = [
'user_id' => $user->getUID(),
'display_name' => $user->getDisplayName(),
- 'email' => $user->getEMailAddress() ? $user->getEMailAddress() : '',
+ 'email' => (string)$user->getSystemEMailAddress(),
'cloud_id' => $user->getCloudId(),
'enabled' => $user->isEnabled(),
'groups' => $groups,
'quota' => $user->getQuota(),
- 'last_seen' => date(\DateTime::ATOM, $user->getLastLogin()), // ISO-8601
+ 'first_seen' => $this->formatLoginDate($user->getFirstLogin()),
+ 'last_seen' => $this->formatLoginDate($user->getLastLogin()),
'user_directory' => $user->getHome(),
'backend' => $user->getBackendClassName()
];
+ } else {
+ $value = $user->getDisplayName();
}
- return $user->getDisplayName();
- }, $users);
- return array_combine($keys, $values);
+ yield $user->getUID() => $value;
+ }
+ }
+
+ private function formatLoginDate(int $timestamp): string {
+ if ($timestamp < 0) {
+ return 'unknown';
+ } elseif ($timestamp === 0) {
+ return 'never';
+ } else {
+ return date(\DateTimeInterface::ATOM, $timestamp); // ISO-8601
+ }
}
}
diff --git a/core/Command/User/Profile.php b/core/Command/User/Profile.php
new file mode 100644
index 00000000000..fd5fbed08cd
--- /dev/null
+++ b/core/Command/User/Profile.php
@@ -0,0 +1,234 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\User;
+
+use OC\Core\Command\Base;
+use OCP\Accounts\IAccount;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\PropertyDoesNotExistException;
+use OCP\IUser;
+use OCP\IUserManager;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Profile extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IAccountManager $accountManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ parent::configure();
+ $this
+ ->setName('user:profile')
+ ->setDescription('Read and modify user profile properties')
+ ->addArgument(
+ 'uid',
+ InputArgument::REQUIRED,
+ 'Account ID used to login'
+ )
+ ->addArgument(
+ 'key',
+ InputArgument::OPTIONAL,
+ 'Profile property to set, get or delete',
+ ''
+ )
+
+ // Get
+ ->addOption(
+ 'default-value',
+ null,
+ InputOption::VALUE_REQUIRED,
+ '(Only applicable on get) If no default value is set and the property does not exist, the command will exit with 1'
+ )
+
+ // Set
+ ->addArgument(
+ 'value',
+ InputArgument::OPTIONAL,
+ 'The new value of the property',
+ null
+ )
+ ->addOption(
+ 'update-only',
+ null,
+ InputOption::VALUE_NONE,
+ 'Only updates the value, if it is not set before, it is not being added'
+ )
+
+ // Delete
+ ->addOption(
+ 'delete',
+ null,
+ InputOption::VALUE_NONE,
+ 'Specify this option to delete the property value'
+ )
+ ->addOption(
+ 'error-if-not-exists',
+ null,
+ InputOption::VALUE_NONE,
+ 'Checks whether the property exists before deleting it'
+ )
+ ;
+ }
+
+ protected function checkInput(InputInterface $input): IUser {
+ $uid = $input->getArgument('uid');
+ $user = $this->userManager->get($uid);
+ if (!$user) {
+ throw new \InvalidArgumentException('The user "' . $uid . '" does not exist.');
+ }
+ // normalize uid
+ $input->setArgument('uid', $user->getUID());
+
+ $key = $input->getArgument('key');
+ if ($key === '') {
+ if ($input->hasParameterOption('--default-value')) {
+ throw new \InvalidArgumentException('The "default-value" option can only be used when specifying a key.');
+ }
+ if ($input->getArgument('value') !== null) {
+ throw new \InvalidArgumentException('The value argument can only be used when specifying a key.');
+ }
+ if ($input->getOption('delete')) {
+ throw new \InvalidArgumentException('The "delete" option can only be used when specifying a key.');
+ }
+ }
+
+ if ($input->getArgument('value') !== null && $input->hasParameterOption('--default-value')) {
+ throw new \InvalidArgumentException('The value argument can not be used together with "default-value".');
+ }
+ if ($input->getOption('update-only') && $input->getArgument('value') === null) {
+ throw new \InvalidArgumentException('The "update-only" option can only be used together with "value".');
+ }
+
+ if ($input->getOption('delete') && $input->hasParameterOption('--default-value')) {
+ throw new \InvalidArgumentException('The "delete" option can not be used together with "default-value".');
+ }
+ if ($input->getOption('delete') && $input->getArgument('value') !== null) {
+ throw new \InvalidArgumentException('The "delete" option can not be used together with "value".');
+ }
+ if ($input->getOption('error-if-not-exists') && !$input->getOption('delete')) {
+ throw new \InvalidArgumentException('The "error-if-not-exists" option can only be used together with "delete".');
+ }
+
+ return $user;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ try {
+ $user = $this->checkInput($input);
+ } catch (\InvalidArgumentException $e) {
+ $output->writeln('<error>' . $e->getMessage() . '</error>');
+ return self::FAILURE;
+ }
+
+ $uid = $input->getArgument('uid');
+ $key = $input->getArgument('key');
+ $userAccount = $this->accountManager->getAccount($user);
+
+ if ($key === '') {
+ $settings = $this->getAllProfileProperties($userAccount);
+ $this->writeArrayInOutputFormat($input, $output, $settings);
+ return self::SUCCESS;
+ }
+
+ $value = $this->getStoredValue($userAccount, $key);
+ $inputValue = $input->getArgument('value');
+ if ($inputValue !== null) {
+ if ($input->hasParameterOption('--update-only') && $value === null) {
+ $output->writeln('<error>The property does not exist for user "' . $uid . '".</error>');
+ return self::FAILURE;
+ }
+
+ return $this->editProfileProperty($output, $userAccount, $key, $inputValue);
+ } elseif ($input->hasParameterOption('--delete')) {
+ if ($input->hasParameterOption('--error-if-not-exists') && $value === null) {
+ $output->writeln('<error>The property does not exist for user "' . $uid . '".</error>');
+ return self::FAILURE;
+ }
+
+ return $this->deleteProfileProperty($output, $userAccount, $key);
+ } elseif ($value !== null) {
+ $output->writeln($value);
+ } elseif ($input->hasParameterOption('--default-value')) {
+ $output->writeln($input->getOption('default-value'));
+ } else {
+ $output->writeln('<error>The property does not exist for user "' . $uid . '".</error>');
+ return self::FAILURE;
+ }
+
+ return self::SUCCESS;
+ }
+
+ private function deleteProfileProperty(OutputInterface $output, IAccount $userAccount, string $key): int {
+ return $this->editProfileProperty($output, $userAccount, $key, '');
+ }
+
+ private function editProfileProperty(OutputInterface $output, IAccount $userAccount, string $key, string $value): int {
+ try {
+ $userAccount->getProperty($key)->setValue($value);
+ } catch (PropertyDoesNotExistException $exception) {
+ $output->writeln('<error>' . $exception->getMessage() . '</error>');
+ return self::FAILURE;
+ }
+
+ $this->accountManager->updateAccount($userAccount);
+ return self::SUCCESS;
+ }
+
+ private function getStoredValue(IAccount $userAccount, string $key): ?string {
+ try {
+ $property = $userAccount->getProperty($key);
+ } catch (PropertyDoesNotExistException) {
+ return null;
+ }
+ return $property->getValue() === '' ? null : $property->getValue();
+ }
+
+ private function getAllProfileProperties(IAccount $userAccount): array {
+ $properties = [];
+
+ foreach ($userAccount->getAllProperties() as $property) {
+ if ($property->getValue() !== '') {
+ $properties[$property->getName()] = $property->getValue();
+ }
+ }
+
+ return $properties;
+ }
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context): array {
+ if ($argumentName === 'uid') {
+ return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord()));
+ }
+ if ($argumentName === 'key') {
+ $userId = $context->getWordAtIndex($context->getWordIndex() - 1);
+ $user = $this->userManager->get($userId);
+ if (!($user instanceof IUser)) {
+ return [];
+ }
+
+ $account = $this->accountManager->getAccount($user);
+
+ $properties = $this->getAllProfileProperties($account);
+ return array_keys($properties);
+ }
+ return [];
+ }
+}
diff --git a/core/Command/User/Report.php b/core/Command/User/Report.php
index 66bc9bd3de1..c0f054adb00 100644
--- a/core/Command/User/Report.php
+++ b/core/Command/User/Report.php
@@ -1,66 +1,52 @@
<?php
+
+declare(strict_types=1);
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @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\Core\Command\User;
+use OC\Files\View;
use OCP\IConfig;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Report extends Command {
- /** @var IUserManager */
- protected $userManager;
- /** @var IConfig */
- private $config;
+ public const DEFAULT_COUNT_DIRS_MAX_USERS = 500;
- /**
- * @param IUserManager $userManager
- */
- public function __construct(IUserManager $userManager, IConfig $config) {
- $this->userManager = $userManager;
- $this->config = $config;
+ public function __construct(
+ protected IUserManager $userManager,
+ private IConfig $config,
+ ) {
parent::__construct();
}
- protected function configure() {
+ protected function configure(): void {
$this
->setName('user:report')
- ->setDescription('shows how many users have access');
+ ->setDescription('shows how many users have access')
+ ->addOption(
+ 'count-dirs',
+ null,
+ InputOption::VALUE_NONE,
+ 'Also count the number of user directories in the database (could time out on huge installations, therefore defaults to no with ' . self::DEFAULT_COUNT_DIRS_MAX_USERS . '+ users)'
+ )
+ ;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$table = new Table($output);
- $table->setHeaders(['User Report', '']);
+ $table->setHeaders(['Account Report', '']);
$userCountArray = $this->countUsers();
+ $total = 0;
if (!empty($userCountArray)) {
- $total = 0;
$rows = [];
foreach ($userCountArray as $classname => $users) {
$total += $users;
@@ -72,10 +58,15 @@ class Report extends Command {
} else {
$rows[] = ['No backend enabled that supports user counting', ''];
}
-
- $userDirectoryCount = $this->countUserDirectories();
$rows[] = [' '];
- $rows[] = ['user directories', $userDirectoryCount];
+
+ if ($total <= self::DEFAULT_COUNT_DIRS_MAX_USERS || $input->getOption('count-dirs')) {
+ $userDirectoryCount = $this->countUserDirectories();
+ $rows[] = ['user directories', $userDirectoryCount];
+ }
+
+ $activeUsers = $this->userManager->countSeenUsers();
+ $rows[] = ['active users', $activeUsers];
$disabledUsers = $this->config->getUsersForUserValue('core', 'enabled', 'false');
$disabledUsersCount = count($disabledUsers);
@@ -86,12 +77,12 @@ class Report extends Command {
return 0;
}
- private function countUsers() {
+ private function countUsers(): array {
return $this->userManager->countUsers();
}
- private function countUserDirectories() {
- $dataview = new \OC\Files\View('/');
+ private function countUserDirectories(): int {
+ $dataview = new View('/');
$userDirectories = $dataview->getDirectoryContent('/', 'httpd/unix-directory');
return count($userDirectories);
}
diff --git a/core/Command/User/ResetPassword.php b/core/Command/User/ResetPassword.php
index bde82fd4770..0e8b1325770 100644
--- a/core/Command/User/ResetPassword.php
+++ b/core/Command/User/ResetPassword.php
@@ -1,36 +1,17 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Andreas Fischer <bantu@owncloud.com>
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Clark Tomlinson <fallen013@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Laurens Post <lkpost@scept.re>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sujith H <sharidasan@owncloud.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\Core\Command\User;
+use OC\Core\Command\Base;
+use OCP\App\IAppManager;
+use OCP\IUser;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -39,13 +20,11 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
-class ResetPassword extends Command {
-
- /** @var IUserManager */
- protected $userManager;
-
- public function __construct(IUserManager $userManager) {
- $this->userManager = $userManager;
+class ResetPassword extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ private IAppManager $appManager,
+ ) {
parent::__construct();
}
@@ -56,13 +35,13 @@ class ResetPassword extends Command {
->addArgument(
'user',
InputArgument::REQUIRED,
- 'Username to reset password'
+ 'Login to reset password'
)
->addOption(
'password-from-env',
null,
InputOption::VALUE_NONE,
- 'read password from environment variable OC_PASS'
+ 'read password from environment variable NC_PASS/OC_PASS'
)
;
}
@@ -77,16 +56,16 @@ class ResetPassword extends Command {
}
if ($input->getOption('password-from-env')) {
- $password = getenv('OC_PASS');
+ $password = getenv('NC_PASS') ?: getenv('OC_PASS');
if (!$password) {
- $output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>');
+ $output->writeln('<error>--password-from-env given, but NC_PASS/OC_PASS is empty!</error>');
return 1;
}
} elseif ($input->isInteractive()) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
- if (\OCP\App::isEnabled('encryption')) {
+ if ($this->appManager->isEnabledForUser('encryption', $user)) {
$output->writeln(
'<error>Warning: Resetting the password when using encryption will result in data loss!</error>'
);
@@ -102,7 +81,7 @@ class ResetPassword extends Command {
$password = $helper->ask($input, $output, $question);
if ($password === null) {
- $output->writeln("<error>Password cannot be empty!</error>");
+ $output->writeln('<error>Password cannot be empty!</error>');
return 1;
}
@@ -111,11 +90,11 @@ class ResetPassword extends Command {
$confirm = $helper->ask($input, $output, $question);
if ($password !== $confirm) {
- $output->writeln("<error>Passwords did not match!</error>");
+ $output->writeln('<error>Passwords did not match!</error>');
return 1;
}
} else {
- $output->writeln("<error>Interactive input or --password-from-env is needed for entering a new password!</error>");
+ $output->writeln('<error>Interactive input or --password-from-env is needed for entering a new password!</error>');
return 1;
}
@@ -128,11 +107,23 @@ class ResetPassword extends Command {
}
if ($success) {
- $output->writeln("<info>Successfully reset password for " . $username . "</info>");
+ $output->writeln('<info>Successfully reset password for ' . $username . '</info>');
} else {
- $output->writeln("<error>Error while resetting password!</error>");
+ $output->writeln('<error>Error while resetting password!</error>');
return 1;
}
return 0;
}
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'user') {
+ return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord()));
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/Setting.php b/core/Command/User/Setting.php
index de621aea182..7fc5aab1dc7 100644
--- a/core/Command/User/Setting.php
+++ b/core/Command/User/Setting.php
@@ -1,61 +1,28 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Johannes Leuker <j.leuker@hosting.de>
- * @author Kim Brose <kim.brose@rwth-aachen.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\Core\Command\User;
use OC\Core\Command\Base;
use OCP\IConfig;
-use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
+use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Setting extends Base {
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IConfig */
- protected $config;
-
- /** @var IDBConnection */
- protected $connection;
-
- /**
- * @param IUserManager $userManager
- * @param IConfig $config
- * @param IDBConnection $connection
- */
- public function __construct(IUserManager $userManager, IConfig $config, IDBConnection $connection) {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IConfig $config,
+ ) {
parent::__construct();
- $this->userManager = $userManager;
- $this->config = $config;
- $this->connection = $connection;
}
protected function configure() {
@@ -66,7 +33,7 @@ class Setting extends Base {
->addArgument(
'uid',
InputArgument::REQUIRED,
- 'User ID used to login'
+ 'Account ID used to login'
)
->addArgument(
'app',
@@ -126,9 +93,14 @@ class Setting extends Base {
}
protected function checkInput(InputInterface $input) {
- $uid = $input->getArgument('uid');
- if (!$input->getOption('ignore-missing-user') && !$this->userManager->userExists($uid)) {
- throw new \InvalidArgumentException('The user "' . $uid . '" does not exist.');
+ if (!$input->getOption('ignore-missing-user')) {
+ $uid = $input->getArgument('uid');
+ $user = $this->userManager->get($uid);
+ if (!$user) {
+ throw new \InvalidArgumentException('The user "' . $uid . '" does not exist.');
+ }
+ // normalize uid
+ $input->setArgument('uid', $user->getUID());
}
if ($input->getArgument('key') === '' && $input->hasParameterOption('--default-value')) {
@@ -179,11 +151,12 @@ class Setting extends Base {
return 1;
}
- if ($app === 'settings' && in_array($key , ['email', 'display_name'])) {
+ if ($app === 'settings' && in_array($key, ['email', 'display_name'])) {
$user = $this->userManager->get($uid);
if ($user instanceof IUser) {
if ($key === 'email') {
- $user->setEMailAddress($input->getArgument('value'));
+ $email = $input->getArgument('value');
+ $user->setSystemEMailAddress(mb_strtolower(trim($email)));
} elseif ($key === 'display_name') {
if (!$user->setDisplayName($input->getArgument('value'))) {
if ($user->getDisplayName() === $input->getArgument('value')) {
@@ -209,7 +182,7 @@ class Setting extends Base {
return 1;
}
- if ($app === 'settings' && in_array($key , ['email', 'display_name'])) {
+ if ($app === 'settings' && in_array($key, ['email', 'display_name'])) {
$user = $this->userManager->get($uid);
if ($user instanceof IUser) {
if ($key === 'email') {
@@ -247,27 +220,45 @@ class Setting extends Base {
}
}
- protected function getUserSettings($uid, $app) {
- $query = $this->connection->getQueryBuilder();
- $query->select('*')
- ->from('preferences')
- ->where($query->expr()->eq('userid', $query->createNamedParameter($uid)));
-
+ protected function getUserSettings(string $uid, string $app): array {
+ $settings = $this->config->getAllUserValues($uid);
if ($app !== '') {
- $query->andWhere($query->expr()->eq('appid', $query->createNamedParameter($app)));
- }
-
- $result = $query->execute();
- $settings = [];
- while ($row = $result->fetch()) {
- $settings[$row['appid']][$row['configkey']] = $row['configvalue'];
+ if (isset($settings[$app])) {
+ $settings = [$app => $settings[$app]];
+ } else {
+ $settings = [];
+ }
}
$user = $this->userManager->get($uid);
- $settings['settings']['display_name'] = $user->getDisplayName();
-
- $result->closeCursor();
+ if ($user !== null) {
+ // Only add the display name if the user exists
+ $settings['settings']['display_name'] = $user->getDisplayName();
+ }
return $settings;
}
+
+ /**
+ * @param string $argumentName
+ * @param CompletionContext $context
+ * @return string[]
+ */
+ public function completeArgumentValues($argumentName, CompletionContext $context) {
+ if ($argumentName === 'uid') {
+ return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord()));
+ }
+ if ($argumentName === 'app') {
+ $userId = $context->getWordAtIndex($context->getWordIndex() - 1);
+ $settings = $this->getUserSettings($userId, '');
+ return array_keys($settings);
+ }
+ if ($argumentName === 'key') {
+ $userId = $context->getWordAtIndex($context->getWordIndex() - 2);
+ $app = $context->getWordAtIndex($context->getWordIndex() - 1);
+ $settings = $this->getUserSettings($userId, $app);
+ return array_keys($settings[$app]);
+ }
+ return [];
+ }
}
diff --git a/core/Command/User/SyncAccountDataCommand.php b/core/Command/User/SyncAccountDataCommand.php
new file mode 100644
index 00000000000..c353df6fe9f
--- /dev/null
+++ b/core/Command/User/SyncAccountDataCommand.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\User;
+
+use OC\Core\Command\Base;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\PropertyDoesNotExistException;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\User\Backend\IGetDisplayNameBackend;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SyncAccountDataCommand extends Base {
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IAccountManager $accountManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('user:sync-account-data')
+ ->setDescription('sync user backend data to accounts table for configured users')
+ ->addOption(
+ 'limit',
+ 'l',
+ InputOption::VALUE_OPTIONAL,
+ 'Number of users to retrieve',
+ '500'
+ )->addOption(
+ 'offset',
+ 'o',
+ InputOption::VALUE_OPTIONAL,
+ 'Offset for retrieving users',
+ '0'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $users = $this->userManager->searchDisplayName('', (int)$input->getOption('limit'), (int)$input->getOption('offset'));
+
+ foreach ($users as $user) {
+ $this->updateUserAccount($user, $output);
+ }
+ return 0;
+ }
+
+ private function updateUserAccount(IUser $user, OutputInterface $output): void {
+ $changed = false;
+ $account = $this->accountManager->getAccount($user);
+ if ($user->getBackend() instanceof IGetDisplayNameBackend) {
+ try {
+ $displayNameProperty = $account->getProperty(IAccountManager::PROPERTY_DISPLAYNAME);
+ } catch (PropertyDoesNotExistException) {
+ $displayNameProperty = null;
+ }
+ if (!$displayNameProperty || $displayNameProperty->getValue() !== $user->getDisplayName()) {
+ $output->writeln($user->getUID() . ' - updating changed display name');
+ $account->setProperty(
+ IAccountManager::PROPERTY_DISPLAYNAME,
+ $user->getDisplayName(),
+ $displayNameProperty ? $displayNameProperty->getScope() : IAccountManager::SCOPE_PRIVATE,
+ $displayNameProperty ? $displayNameProperty->getVerified() : IAccountManager::NOT_VERIFIED,
+ $displayNameProperty ? $displayNameProperty->getVerificationData() : ''
+ );
+ $changed = true;
+ }
+ }
+
+ if ($changed) {
+ $this->accountManager->updateAccount($account);
+ $output->writeln($user->getUID() . ' - account data updated');
+ } else {
+ $output->writeln($user->getUID() . ' - nothing to update');
+ }
+ }
+}
diff --git a/core/Command/User/Welcome.php b/core/Command/User/Welcome.php
new file mode 100644
index 00000000000..65637759689
--- /dev/null
+++ b/core/Command/User/Welcome.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 FedericoHeichou <federicoheichou@gmail.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Command\User;
+
+use OC\Core\Command\Base;
+use OCA\Settings\Mailer\NewUserMailHelper;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Welcome extends Base {
+ /**
+ * @param IUserManager $userManager
+ * @param NewUserMailHelper $newUserMailHelper
+ */
+ public function __construct(
+ protected IUserManager $userManager,
+ private NewUserMailHelper $newUserMailHelper,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @return void
+ */
+ protected function configure() {
+ $this
+ ->setName('user:welcome')
+ ->setDescription('Sends the welcome email')
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'The user to send the email to'
+ )
+ ->addOption(
+ 'reset-password',
+ 'r',
+ InputOption::VALUE_NONE,
+ 'Add the reset password link to the email'
+ )
+ ;
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument('user');
+ // check if user exists
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln('<error>User does not exist</error>');
+ return 1;
+ }
+ $email = $user->getEMailAddress();
+ if ($email === '' || $email === null) {
+ $output->writeln('<error>User does not have an email address</error>');
+ return 1;
+ }
+ try {
+ $emailTemplate = $this->newUserMailHelper->generateTemplate($user, $input->getOption('reset-password'));
+ $this->newUserMailHelper->sendMail($user, $emailTemplate);
+ } catch (\Exception $e) {
+ $output->writeln('<error>Failed to send email: ' . $e->getMessage() . '</error>');
+ return 1;
+ }
+ return 0;
+ }
+}