diff options
Diffstat (limited to 'core/Command/User')
-rw-r--r-- | core/Command/User/Add.php | 122 | ||||
-rw-r--r-- | core/Command/User/AuthTokens/Add.php (renamed from core/Command/User/AddAppPassword.php) | 62 | ||||
-rw-r--r-- | core/Command/User/AuthTokens/Delete.php | 104 | ||||
-rw-r--r-- | core/Command/User/AuthTokens/ListCommand.php | 84 | ||||
-rw-r--r-- | core/Command/User/ClearGeneratedAvatarCacheCommand.php | 35 | ||||
-rw-r--r-- | core/Command/User/Delete.php | 36 | ||||
-rw-r--r-- | core/Command/User/Disable.php | 31 | ||||
-rw-r--r-- | core/Command/User/Enable.php | 31 | ||||
-rw-r--r-- | core/Command/User/Info.php | 50 | ||||
-rw-r--r-- | core/Command/User/Keys/Verify.php | 83 | ||||
-rw-r--r-- | core/Command/User/LastSeen.php | 93 | ||||
-rw-r--r-- | core/Command/User/ListCommand.php | 79 | ||||
-rw-r--r-- | core/Command/User/Profile.php | 234 | ||||
-rw-r--r-- | core/Command/User/Report.php | 39 | ||||
-rw-r--r-- | core/Command/User/ResetPassword.php | 56 | ||||
-rw-r--r-- | core/Command/User/Setting.php | 48 | ||||
-rw-r--r-- | core/Command/User/SyncAccountDataCommand.php | 84 | ||||
-rw-r--r-- | core/Command/User/Welcome.php | 78 |
18 files changed, 970 insertions, 379 deletions
diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php index 24d11fbee6e..4de4e247991 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -1,35 +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; @@ -39,57 +27,80 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; class Add extends Command { - protected IUserManager $userManager; - protected 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'); @@ -103,29 +114,28 @@ class Add 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 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; } @@ -153,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/AuthTokens/Add.php index ec39cdc974e..89b20535c63 100644 --- a/core/Command/User/AddAppPassword.php +++ b/core/Command/User/AuthTokens/Add.php @@ -3,28 +3,10 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, NextCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Sean Molenaar <sean@seanmolenaar.eu> - * - * @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: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace OC\Core\Command\User; +namespace OC\Core\Command\User\AuthTokens; use OC\Authentication\Events\AppPasswordCreatedEvent; use OC\Authentication\Token\IProvider; @@ -40,31 +22,25 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; -class AddAppPassword extends Command { - protected IUserManager $userManager; - protected IProvider $tokenProvider; - private ISecureRandom $random; - private IEventDispatcher $eventDispatcher; - - public function __construct(IUserManager $userManager, - IProvider $tokenProvider, - ISecureRandom $random, - IEventDispatcher $eventDispatcher) { - $this->tokenProvider = $tokenProvider; - $this->userManager = $userManager; - $this->random = $random; - $this->eventDispatcher = $eventDispatcher; +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:add-app-password') - ->setDescription('Add app password for the named user') + ->setName('user:auth-tokens:add') + ->setAliases(['user:add-app-password']) + ->setDescription('Add app password for the named account') ->addArgument( 'user', InputArgument::REQUIRED, - 'Username to add app password for' + 'Login to add app password for' ) ->addOption( 'password-from-env', @@ -81,21 +57,21 @@ class AddAppPassword extends Command { $user = $this->userManager->get($username); if (is_null($user)) { - $output->writeln('<error>User does not exist</error>'); + $output->writeln('<error>Account does not exist</error>'); return 1; } if ($input->getOption('password-from-env')) { - $password = getenv('NC_PASS') ?? getenv('OC_PASS'); + $password = getenv('NC_PASS') ?: getenv('OC_PASS'); if (!$password) { - $output->writeln('<error>--password-from-env given, but NC_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'); - $question = new Question('Enter the user password: '); + $question = new Question('Enter the account password: '); $question->setHidden(true); /** @var null|string $password */ $password = $helper->ask($input, $output, $question); @@ -105,7 +81,7 @@ class AddAppPassword extends Command { $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); + $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $generatedToken = $this->tokenProvider->generateToken( $token, $user->getUID(), 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 9624f04fa18..c5d0578f5f8 100644 --- a/core/Command/User/Delete.php +++ b/core/Command/User/Delete.php @@ -1,26 +1,9 @@ <?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 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; @@ -33,14 +16,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Delete extends Base { - /** @var IUserManager */ - protected $userManager; - - /** - * @param IUserManager $userManager - */ - public function __construct(IUserManager $userManager) { - $this->userManager = $userManager; + public function __construct( + protected IUserManager $userManager, + ) { parent::__construct(); } diff --git a/core/Command/User/Disable.php b/core/Command/User/Disable.php index bc819f39e1d..4713950bf30 100644 --- a/core/Command/User/Disable.php +++ b/core/Command/User/Disable.php @@ -1,25 +1,9 @@ <?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; @@ -32,10 +16,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Disable extends Base { - protected IUserManager $userManager; - - public function __construct(IUserManager $userManager) { - $this->userManager = $userManager; + public function __construct( + protected IUserManager $userManager, + ) { parent::__construct(); } diff --git a/core/Command/User/Enable.php b/core/Command/User/Enable.php index f4e16eec4af..23f56e5dd4f 100644 --- a/core/Command/User/Enable.php +++ b/core/Command/User/Enable.php @@ -1,25 +1,9 @@ <?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; @@ -32,10 +16,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Enable extends Base { - protected IUserManager $userManager; - - public function __construct(IUserManager $userManager) { - $this->userManager = $userManager; + public function __construct( + protected IUserManager $userManager, + ) { parent::__construct(); } diff --git a/core/Command/User/Info.php b/core/Command/User/Info.php index 1e89a8d0911..e7fc9286e74 100644 --- a/core/Command/User/Info.php +++ b/core/Command/User/Info.php @@ -1,30 +1,13 @@ <?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; @@ -35,12 +18,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Info extends Base { - protected IUserManager $userManager; - protected 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(); } @@ -77,7 +58,8 @@ class Info extends Base { 'groups' => $groups, 'quota' => $user->getQuota(), 'storage' => $this->getStorageInfo($user), - 'last_seen' => date(\DateTimeInterface::ATOM, $user->getLastLogin()), // ISO-8601 + 'first_seen' => $this->formatLoginDate($user->getFirstLogin()), + 'last_seen' => $this->formatLoginDate($user->getLastLogin()), 'user_directory' => $user->getHome(), 'backend' => $user->getBackendClassName() ]; @@ -85,6 +67,16 @@ class Info extends Base { 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 @@ -94,7 +86,7 @@ class Info extends Base { \OC_Util::setupFS($user->getUID()); try { $storage = \OC_Helper::getStorageInfo('/'); - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { return []; } 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 5ea6c64d249..984def72cd6 100644 --- a/core/Command/User/LastSeen.php +++ b/core/Command/User/LastSeen.php @@ -1,27 +1,9 @@ <?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 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; @@ -31,44 +13,71 @@ 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 LastSeen extends Base { - protected IUserManager $userManager; - - public function __construct(IUserManager $userManager) { - $this->userManager = $userManager; + 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; } diff --git a/core/Command/User/ListCommand.php b/core/Command/User/ListCommand.php index c254a8a11cf..66b831c793b 100644 --- a/core/Command/User/ListCommand.php +++ b/core/Command/User/ListCommand.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <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; @@ -33,13 +15,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ListCommand extends Base { - protected IUserManager $userManager; - protected 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(); } @@ -48,6 +27,11 @@ 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, @@ -74,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; @@ -82,18 +70,13 @@ 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' => (string)$user->getSystemEMailAddress(), @@ -101,13 +84,25 @@ class ListCommand extends Base { 'enabled' => $user->isEnabled(), 'groups' => $groups, 'quota' => $user->getQuota(), - 'last_seen' => date(\DateTimeInterface::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 e080a617258..c0f054adb00 100644 --- a/core/Command/User/Report.php +++ b/core/Command/User/Report.php @@ -3,29 +3,9 @@ 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; @@ -41,13 +21,10 @@ use Symfony\Component\Console\Output\OutputInterface; class Report extends Command { public const DEFAULT_COUNT_DIRS_MAX_USERS = 500; - protected IUserManager $userManager; - private IConfig $config; - - public function __construct(IUserManager $userManager, - IConfig $config) { - $this->userManager = $userManager; - $this->config = $config; + public function __construct( + protected IUserManager $userManager, + private IConfig $config, + ) { parent::__construct(); } @@ -66,7 +43,7 @@ class Report extends Command { 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)) { diff --git a/core/Command/User/ResetPassword.php b/core/Command/User/ResetPassword.php index 294cea38b71..0e8b1325770 100644 --- a/core/Command/User/ResetPassword.php +++ b/core/Command/User/ResetPassword.php @@ -1,29 +1,9 @@ <?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 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; @@ -41,13 +21,11 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; class ResetPassword extends Base { - protected IUserManager $userManager; - private IAppManager $appManager; - - public function __construct(IUserManager $userManager, IAppManager $appManager) { + public function __construct( + protected IUserManager $userManager, + private IAppManager $appManager, + ) { parent::__construct(); - $this->userManager = $userManager; - $this->appManager = $appManager; } protected function configure() { @@ -57,13 +35,13 @@ class ResetPassword extends Base { ->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' ) ; } @@ -78,9 +56,9 @@ class ResetPassword extends Base { } 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()) { @@ -103,7 +81,7 @@ class ResetPassword extends Base { $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; } @@ -112,11 +90,11 @@ class ResetPassword extends Base { $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; } @@ -129,9 +107,9 @@ class ResetPassword extends Base { } 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; diff --git a/core/Command/User/Setting.php b/core/Command/User/Setting.php index fac5c3c976c..7fc5aab1dc7 100644 --- a/core/Command/User/Setting.php +++ b/core/Command/User/Setting.php @@ -1,27 +1,9 @@ <?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; @@ -36,13 +18,11 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Setting extends Base { - protected IUserManager $userManager; - protected IConfig $config; - - public function __construct(IUserManager $userManager, IConfig $config) { + public function __construct( + protected IUserManager $userManager, + protected IConfig $config, + ) { parent::__construct(); - $this->userManager = $userManager; - $this->config = $config; } protected function configure() { @@ -53,7 +33,7 @@ class Setting extends Base { ->addArgument( 'uid', InputArgument::REQUIRED, - 'User ID used to login' + 'Account ID used to login' ) ->addArgument( 'app', @@ -175,7 +155,8 @@ class Setting extends Base { $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')) { @@ -239,7 +220,7 @@ class Setting extends Base { } } - protected function getUserSettings($uid, $app) { + protected function getUserSettings(string $uid, string $app): array { $settings = $this->config->getAllUserValues($uid); if ($app !== '') { if (isset($settings[$app])) { @@ -250,7 +231,10 @@ class Setting extends Base { } $user = $this->userManager->get($uid); - $settings['settings']['display_name'] = $user->getDisplayName(); + if ($user !== null) { + // Only add the display name if the user exists + $settings['settings']['display_name'] = $user->getDisplayName(); + } return $settings; } 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; + } +} |