diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2024-03-15 15:55:27 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-15 15:55:27 +0100 |
commit | 8201a93452601d039576e1c7f540a30e0aa4b9ef (patch) | |
tree | ef6566aad1afb1181a5f4d805bef32956c0cef73 /core/Command | |
parent | a676a67b9ecdb3ec83b6e2e7077a5fa68d220893 (diff) | |
parent | e0faca6170e9bf39bfe34fcac225d4ae2ae8c3b7 (diff) | |
download | nextcloud-server-8201a93452601d039576e1c7f540a30e0aa4b9ef.tar.gz nextcloud-server-8201a93452601d039576e1c7f540a30e0aa4b9ef.zip |
Merge branch 'master' into show-enabled-and-disabled-apps
Signed-off-by: John Molakvoæ <skjnldsv@users.noreply.github.com>
Diffstat (limited to 'core/Command')
108 files changed, 2706 insertions, 1338 deletions
diff --git a/core/Command/App/Disable.php b/core/Command/App/Disable.php index 05d35053b13..53a13765342 100644 --- a/core/Command/App/Disable.php +++ b/core/Command/App/Disable.php @@ -33,12 +33,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Disable extends Command implements CompletionAwareInterface { - protected IAppManager $appManager; protected int $exitCode = 0; - public function __construct(IAppManager $appManager) { + public function __construct( + protected IAppManager $appManager, + ) { parent::__construct(); - $this->appManager = $appManager; } protected function configure(): void { @@ -70,7 +70,7 @@ class Disable extends Command implements CompletionAwareInterface { try { $this->appManager->disableApp($appId); - $appVersion = \OC_App::getAppVersion($appId); + $appVersion = $this->appManager->getAppVersion($appId); $output->writeln($appId . ' ' . $appVersion . ' disabled'); } catch (\Exception $e) { $output->writeln($e->getMessage()); @@ -83,7 +83,7 @@ class Disable extends Command implements CompletionAwareInterface { * @param CompletionContext $context * @return string[] */ - public function completeOptionValues($optionName, CompletionContext $context) { + public function completeOptionValues($optionName, CompletionContext $context): array { return []; } @@ -92,7 +92,7 @@ class Disable extends Command implements CompletionAwareInterface { * @param CompletionContext $context * @return string[] */ - public function completeArgumentValues($argumentName, CompletionContext $context) { + public function completeArgumentValues($argumentName, CompletionContext $context): array { if ($argumentName === 'app-id') { return array_diff(\OC_App::getEnabledApps(true, true), $this->appManager->getAlwaysEnabledApps()); } diff --git a/core/Command/App/Enable.php b/core/Command/App/Enable.php index c7a071e27b5..624b31521ad 100644 --- a/core/Command/App/Enable.php +++ b/core/Command/App/Enable.php @@ -39,14 +39,13 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Enable extends Command implements CompletionAwareInterface { - protected IAppManager $appManager; - protected IGroupManager $groupManager; protected int $exitCode = 0; - public function __construct(IAppManager $appManager, IGroupManager $groupManager) { + public function __construct( + protected IAppManager $appManager, + protected IGroupManager $groupManager, + ) { parent::__construct(); - $this->appManager = $appManager; - $this->groupManager = $groupManager; } protected function configure(): void { @@ -109,7 +108,7 @@ class Enable extends Command implements CompletionAwareInterface { } $installer->installApp($appId, $forceEnable); - $appVersion = \OC_App::getAppVersion($appId); + $appVersion = $this->appManager->getAppVersion($appId); if ($groupIds === []) { $this->appManager->enableApp($appId, $forceEnable); @@ -147,7 +146,7 @@ class Enable extends Command implements CompletionAwareInterface { * @param CompletionContext $context * @return string[] */ - public function completeOptionValues($optionName, CompletionContext $context) { + public function completeOptionValues($optionName, CompletionContext $context): array { if ($optionName === 'groups') { return array_map(function (IGroup $group) { return $group->getGID(); @@ -161,7 +160,7 @@ class Enable extends Command implements CompletionAwareInterface { * @param CompletionContext $context * @return string[] */ - public function completeArgumentValues($argumentName, CompletionContext $context) { + public function completeArgumentValues($argumentName, CompletionContext $context): array { if ($argumentName === 'app-id') { $allApps = \OC_App::getAllApps(); return array_diff($allApps, \OC_App::getEnabledApps(true, true)); diff --git a/core/Command/App/GetPath.php b/core/Command/App/GetPath.php index 2ec72385191..ea614070e7d 100644 --- a/core/Command/App/GetPath.php +++ b/core/Command/App/GetPath.php @@ -29,7 +29,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class GetPath extends Base { - protected function configure() { + protected function configure(): void { parent::configure(); $this @@ -67,7 +67,7 @@ class GetPath extends Base { * @param CompletionContext $context * @return string[] */ - public function completeArgumentValues($argumentName, CompletionContext $context) { + public function completeArgumentValues($argumentName, CompletionContext $context): array { if ($argumentName === 'app') { return \OC_App::getAllApps(); } diff --git a/core/Command/App/Install.php b/core/Command/App/Install.php index a699a2e7af0..2d02fff4dbf 100644 --- a/core/Command/App/Install.php +++ b/core/Command/App/Install.php @@ -28,6 +28,7 @@ namespace OC\Core\Command\App; use OC\Installer; +use OCP\App\IAppManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -35,7 +36,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Install extends Command { - protected function configure() { + protected function configure(): void { $this ->setName('app:install') ->setDescription('install an app') @@ -89,7 +90,7 @@ class Install extends Command { return 1; } - $appVersion = \OC_App::getAppVersion($appId); + $appVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId); $output->writeln($appId . ' ' . $appVersion . ' installed'); if (!$input->getOption('keep-disabled')) { diff --git a/core/Command/App/ListApps.php b/core/Command/App/ListApps.php index 6cb91aae39c..a361ac6b19e 100644 --- a/core/Command/App/ListApps.php +++ b/core/Command/App/ListApps.php @@ -34,14 +34,13 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ListApps extends Base { - protected IAppManager $manager; - - public function __construct(IAppManager $manager) { + public function __construct( + protected IAppManager $manager, + ) { parent::__construct(); - $this->manager = $manager; } - protected function configure() { + protected function configure(): void { parent::configure(); $this @@ -124,7 +123,7 @@ class ListApps extends Base { sort($disabledApps); foreach ($disabledApps as $app) { - $apps['disabled'][$app] = $versions[$app] ?? null; + $apps['disabled'][$app] = $this->manager->getAppVersion($app) . (isset($versions[$app]) ? ' (installed ' . $versions[$app] . ')' : ''); } } @@ -137,7 +136,7 @@ class ListApps extends Base { * @param OutputInterface $output * @param array $items */ - protected function writeAppList(InputInterface $input, OutputInterface $output, $items) { + protected function writeAppList(InputInterface $input, OutputInterface $output, $items): void { switch ($input->getOption('output')) { case self::OUTPUT_FORMAT_PLAIN: if (isset($items['enabled'])) { @@ -149,11 +148,11 @@ class ListApps extends Base { $output->writeln('Disabled:'); parent::writeArrayInOutputFormat($input, $output, $items['disabled']); } - break; + break; default: parent::writeArrayInOutputFormat($input, $output, $items); - break; + break; } } @@ -162,7 +161,7 @@ class ListApps extends Base { * @param CompletionContext $context * @return array */ - public function completeOptionValues($optionName, CompletionContext $context) { + public function completeOptionValues($optionName, CompletionContext $context): array { if ($optionName === 'shipped') { return ['true', 'false']; } @@ -174,7 +173,7 @@ class ListApps extends Base { * @param CompletionContext $context * @return string[] */ - public function completeArgumentValues($argumentName, CompletionContext $context) { + public function completeArgumentValues($argumentName, CompletionContext $context): array { return []; } } diff --git a/core/Command/App/Remove.php b/core/Command/App/Remove.php index 2aa453132e4..5fa05079bd8 100644 --- a/core/Command/App/Remove.php +++ b/core/Command/App/Remove.php @@ -39,18 +39,15 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; class Remove extends Command implements CompletionAwareInterface { - protected IAppManager $manager; - private Installer $installer; - private LoggerInterface $logger; - - public function __construct(IAppManager $manager, Installer $installer, LoggerInterface $logger) { + public function __construct( + protected IAppManager $manager, + private Installer $installer, + private LoggerInterface $logger, + ) { parent::__construct(); - $this->manager = $manager; - $this->installer = $installer; - $this->logger = $logger; } - protected function configure() { + protected function configure(): void { $this ->setName('app:remove') ->setDescription('remove an app') @@ -116,7 +113,7 @@ class Remove extends Command implements CompletionAwareInterface { return 1; } - $appVersion = \OC_App::getAppVersion($appId); + $appVersion = $this->manager->getAppVersion($appId); $output->writeln($appId . ' ' . $appVersion . ' removed'); return 0; @@ -127,7 +124,7 @@ class Remove extends Command implements CompletionAwareInterface { * @param CompletionContext $context * @return string[] */ - public function completeOptionValues($optionName, CompletionContext $context) { + public function completeOptionValues($optionName, CompletionContext $context): array { return []; } @@ -136,7 +133,7 @@ class Remove extends Command implements CompletionAwareInterface { * @param CompletionContext $context * @return string[] */ - public function completeArgumentValues($argumentName, CompletionContext $context) { + public function completeArgumentValues($argumentName, CompletionContext $context): array { if ($argumentName === 'app-id') { return \OC_App::getAllApps(); } diff --git a/core/Command/App/Update.php b/core/Command/App/Update.php index 6a6d43c28e5..c8e62cb5b71 100644 --- a/core/Command/App/Update.php +++ b/core/Command/App/Update.php @@ -37,18 +37,15 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Update extends Command { - protected IAppManager $manager; - private Installer $installer; - private LoggerInterface $logger; - - public function __construct(IAppManager $manager, Installer $installer, LoggerInterface $logger) { + public function __construct( + protected IAppManager $manager, + private Installer $installer, + private LoggerInterface $logger, + ) { parent::__construct(); - $this->manager = $manager; - $this->installer = $installer; - $this->logger = $logger; } - protected function configure() { + protected function configure(): void { $this ->setName('app:update') ->setDescription('update an app or all apps') @@ -80,6 +77,7 @@ class Update extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $singleAppId = $input->getArgument('app-id'); + $updateFound = false; if ($singleAppId) { $apps = [$singleAppId]; @@ -100,6 +98,7 @@ class Update extends Command { foreach ($apps as $appId) { $newVersion = $this->installer->isUpdateAvailable($appId, $input->getOption('allow-unstable')); if ($newVersion) { + $updateFound = true; $output->writeln($appId . ' new version available: ' . $newVersion); if (!$input->getOption('showonly')) { @@ -111,6 +110,7 @@ class Update extends Command { 'exception' => $e, ]); $output->writeln('Error: ' . $e->getMessage()); + $result = false; $return = 1; } @@ -124,6 +124,14 @@ class Update extends Command { } } + if (!$updateFound) { + if ($singleAppId) { + $output->writeln($singleAppId . ' is up-to-date or no updates could be found'); + } else { + $output->writeln('All apps are up-to-date or no updates could be found'); + } + } + return $return; } } diff --git a/core/Command/Background/Ajax.php b/core/Command/Background/Ajax.php index 5dc94d939d7..41b80eb531f 100644 --- a/core/Command/Background/Ajax.php +++ b/core/Command/Background/Ajax.php @@ -26,7 +26,7 @@ namespace OC\Core\Command\Background; class Ajax extends Base { - protected function getMode() { + protected function getMode(): string { return 'ajax'; } } diff --git a/core/Command/Background/Base.php b/core/Command/Background/Base.php index dca7b58a5fc..715596f9979 100644 --- a/core/Command/Background/Base.php +++ b/core/Command/Background/Base.php @@ -38,14 +38,14 @@ use Symfony\Component\Console\Output\OutputInterface; */ abstract class Base extends Command { abstract protected function getMode(); - protected IConfig $config; - public function __construct(IConfig $config) { + public function __construct( + protected IConfig $config, + ) { parent::__construct(); - $this->config = $config; } - protected function configure() { + protected function configure(): void { $mode = $this->getMode(); $this ->setName("background:$mode") @@ -59,6 +59,7 @@ abstract class Base extends Command { * * @param InputInterface $input * @param OutputInterface $output + * @return int */ protected function execute(InputInterface $input, OutputInterface $output): int { $mode = $this->getMode(); diff --git a/core/Command/Background/Cron.php b/core/Command/Background/Cron.php index 9dbb4f855e5..665919b5ae9 100644 --- a/core/Command/Background/Cron.php +++ b/core/Command/Background/Cron.php @@ -26,7 +26,7 @@ namespace OC\Core\Command\Background; class Cron extends Base { - protected function getMode() { + protected function getMode(): string { return 'cron'; } } diff --git a/core/Command/Background/Delete.php b/core/Command/Background/Delete.php new file mode 100644 index 00000000000..8d57d18dfd0 --- /dev/null +++ b/core/Command/Background/Delete.php @@ -0,0 +1,80 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2024, Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @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/>. + * + */ + +namespace OC\Core\Command\Background; + +use OC\Core\Command\Base; +use OCP\BackgroundJob\IJobList; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class Delete extends Base { + public function __construct( + protected IJobList $jobList, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('background-job:delete') + ->setDescription('Remove a background job from database') + ->addArgument( + 'job-id', + InputArgument::REQUIRED, + 'The ID of the job in the database' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $jobId = (int) $input->getArgument('job-id'); + + $job = $this->jobList->getById($jobId); + if ($job === null) { + $output->writeln('<error>Job with ID ' . $jobId . ' could not be found in the database</error>'); + return 1; + } + + $output->writeln('Job class: ' . get_class($job)); + $output->writeln('Arguments: ' . json_encode($job->getArgument())); + $output->writeln(''); + + $question = new ConfirmationQuestion( + '<comment>Do you really want to delete this background job ? It could create some misbehaviours in Nextcloud.</comment> (y/N) ', false, + '/^(y|Y)/i' + ); + + $helper = $this->getHelper('question'); + if (!$helper->ask($input, $output, $question)) { + $output->writeln('aborted.'); + return 0; + } + + $this->jobList->remove($job, $job->getArgument()); + return 0; + } +} diff --git a/core/Command/Background/Job.php b/core/Command/Background/Job.php index 823498cf8ca..bc03f896361 100644 --- a/core/Command/Background/Job.php +++ b/core/Command/Background/Job.php @@ -27,7 +27,6 @@ namespace OC\Core\Command\Background; use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; -use OCP\ILogger; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -35,14 +34,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Job extends Command { - protected IJobList $jobList; - protected ILogger $logger; - - public function __construct(IJobList $jobList, - ILogger $logger) { + public function __construct( + protected IJobList $jobList, + ) { parent::__construct(); - $this->jobList = $jobList; - $this->logger = $logger; } protected function configure(): void { @@ -89,14 +84,15 @@ class Job extends Command { $output->writeln('<error>Something went wrong when trying to retrieve Job with ID ' . $jobId . ' from database</error>'); return 1; } - $job->execute($this->jobList, $this->logger); + /** @psalm-suppress DeprecatedMethod Calling execute until it is removed, then will switch to start */ + $job->execute($this->jobList); $job = $this->jobList->getById($jobId); if (($job === null) || ($lastRun !== $job->getLastRun())) { $output->writeln('<info>Job executed!</info>'); $output->writeln(''); - if ($job instanceof \OC\BackgroundJob\TimedJob || $job instanceof \OCP\BackgroundJob\TimedJob) { + if ($job instanceof \OCP\BackgroundJob\TimedJob) { $this->printJobInfo($jobId, $job, $output); } } else { @@ -107,7 +103,7 @@ class Job extends Command { return 0; } - protected function printJobInfo(int $jobId, IJob $job, OutputInterface$output): void { + protected function printJobInfo(int $jobId, IJob $job, OutputInterface $output): void { $row = $this->jobList->getDetailsById($jobId); $lastRun = new \DateTime(); @@ -120,10 +116,10 @@ class Job extends Command { $output->writeln('Job class: ' . get_class($job)); $output->writeln('Arguments: ' . json_encode($job->getArgument())); - $isTimedJob = $job instanceof \OC\BackgroundJob\TimedJob || $job instanceof \OCP\BackgroundJob\TimedJob; + $isTimedJob = $job instanceof \OCP\BackgroundJob\TimedJob; if ($isTimedJob) { $output->writeln('Type: timed'); - } elseif ($job instanceof \OC\BackgroundJob\QueuedJob || $job instanceof \OCP\BackgroundJob\QueuedJob) { + } elseif ($job instanceof \OCP\BackgroundJob\QueuedJob) { $output->writeln('Type: queued'); } else { $output->writeln('Type: job'); diff --git a/core/Command/Background/ListCommand.php b/core/Command/Background/ListCommand.php index bdd45f3a25f..ef5b478a197 100644 --- a/core/Command/Background/ListCommand.php +++ b/core/Command/Background/ListCommand.php @@ -32,11 +32,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ListCommand extends Base { - protected IJobList $jobList; - - public function __construct(IJobList $jobList) { + public function __construct( + protected IJobList $jobList, + ) { parent::__construct(); - $this->jobList = $jobList; } protected function configure(): void { @@ -54,7 +53,7 @@ class ListCommand extends Base { 'l', InputOption::VALUE_OPTIONAL, 'Number of jobs to retrieve', - '10' + '500' )->addOption( 'offset', 'o', @@ -67,20 +66,25 @@ class ListCommand extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $jobs = $this->jobList->getJobs($input->getOption('class'), (int)$input->getOption('limit'), (int)$input->getOption('offset')); - $this->writeTableInOutputFormat($input, $output, $this->formatJobs($jobs)); + $limit = (int)$input->getOption('limit'); + $jobsInfo = $this->formatJobs($this->jobList->getJobsIterator($input->getOption('class'), $limit, (int)$input->getOption('offset'))); + $this->writeTableInOutputFormat($input, $output, $jobsInfo); + if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN && count($jobsInfo) >= $limit) { + $output->writeln("\n<comment>Output is currently limited to " . $limit . " jobs. Specify `-l, --limit[=LIMIT]` to override.</comment>"); + } return 0; } - protected function formatJobs(array $jobs): array { - return array_map( - fn ($job) => [ + protected function formatJobs(iterable $jobs): array { + $jobsInfo = []; + foreach ($jobs as $job) { + $jobsInfo[] = [ 'id' => $job->getId(), 'class' => get_class($job), 'last_run' => date(DATE_ATOM, $job->getLastRun()), 'argument' => json_encode($job->getArgument()), - ], - $jobs - ); + ]; + } + return $jobsInfo; } } diff --git a/core/Command/Background/WebCron.php b/core/Command/Background/WebCron.php index 7da379b6a53..59bf4746c40 100644 --- a/core/Command/Background/WebCron.php +++ b/core/Command/Background/WebCron.php @@ -26,7 +26,7 @@ namespace OC\Core\Command\Background; class WebCron extends Base { - protected function getMode() { + protected function getMode(): string { return 'webcron'; } } diff --git a/core/Command/Base.php b/core/Command/Base.php index abf9f95773a..f8b864c5864 100644 --- a/core/Command/Base.php +++ b/core/Command/Base.php @@ -26,8 +26,8 @@ namespace OC\Core\Command; use OC\Core\Command\User\ListCommand; -use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; +use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; @@ -161,7 +161,7 @@ class Base extends Command implements CompletionAwareInterface { * * Gives a chance to the command to properly terminate what it's doing */ - protected function cancelOperation() { + public function cancelOperation(): void { $this->interrupted = true; } diff --git a/core/Command/Broadcast/Test.php b/core/Command/Broadcast/Test.php index 7a67c983f79..22a65d86e7b 100644 --- a/core/Command/Broadcast/Test.php +++ b/core/Command/Broadcast/Test.php @@ -34,11 +34,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Test extends Command { - private IEventDispatcher $eventDispatcher; - - public function __construct(IEventDispatcher $eventDispatcher) { + public function __construct( + private IEventDispatcher $eventDispatcher, + ) { parent::__construct(); - $this->eventDispatcher = $eventDispatcher; } protected function configure(): void { @@ -69,7 +68,7 @@ class Test extends Command { private $uid; public function __construct(string $name, - string $uid) { + string $uid) { parent::__construct(); $this->name = $name; $this->uid = $uid; diff --git a/core/Command/Check.php b/core/Command/Check.php index 18c45323f37..478486b7dee 100644 --- a/core/Command/Check.php +++ b/core/Command/Check.php @@ -29,11 +29,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Check extends Base { - private SystemConfig $config; - - public function __construct(SystemConfig $config) { + public function __construct( + private SystemConfig $config, + ) { parent::__construct(); - $this->config = $config; } protected function configure() { diff --git a/core/Command/Config/App/DeleteConfig.php b/core/Command/Config/App/DeleteConfig.php index 0da1e965bd0..b77f27ccd07 100644 --- a/core/Command/Config/App/DeleteConfig.php +++ b/core/Command/Config/App/DeleteConfig.php @@ -28,14 +28,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class DeleteConfig extends Base { - protected IConfig $config; - - /** - * @param IConfig $config - */ - public function __construct(IConfig $config) { + public function __construct( + protected IConfig $config, + ) { parent::__construct(); - $this->config = $config; } protected function configure() { diff --git a/core/Command/Config/App/GetConfig.php b/core/Command/Config/App/GetConfig.php index 7fdff2be732..f85f978cc61 100644 --- a/core/Command/Config/App/GetConfig.php +++ b/core/Command/Config/App/GetConfig.php @@ -1,8 +1,11 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Joas Schilling <coding@schilljs.com> + * @author Maxence Lange <maxence@artificial-owl.com> * * @license AGPL-3.0 * @@ -21,18 +24,18 @@ */ namespace OC\Core\Command\Config\App; -use OCP\IConfig; +use OCP\Exceptions\AppConfigUnknownKeyException; +use OCP\IAppConfig; 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 GetConfig extends Base { - protected IConfig $config; - - public function __construct(IConfig $config) { + public function __construct( + protected IAppConfig $appConfig, + ) { parent::__construct(); - $this->config = $config; } protected function configure() { @@ -52,6 +55,12 @@ class GetConfig extends Base { 'Name of the config to get' ) ->addOption( + 'details', + null, + InputOption::VALUE_NONE, + 'returns complete details about the app config value' + ) + ->addOption( 'default-value', null, InputOption::VALUE_OPTIONAL, @@ -72,14 +81,21 @@ class GetConfig extends Base { $configName = $input->getArgument('name'); $defaultValue = $input->getOption('default-value'); - if (!in_array($configName, $this->config->getAppKeys($appName)) && !$input->hasParameterOption('--default-value')) { - return 1; + if ($input->getOption('details')) { + $details = $this->appConfig->getDetails($appName, $configName); + $details['type'] = $details['typeString']; + unset($details['typeString']); + $this->writeArrayInOutputFormat($input, $output, $details); + return 0; } - if (!in_array($configName, $this->config->getAppKeys($appName))) { + try { + $configValue = $this->appConfig->getDetails($appName, $configName)['value']; + } catch (AppConfigUnknownKeyException $e) { + if (!$input->hasParameterOption('--default-value')) { + return 1; + } $configValue = $defaultValue; - } else { - $configValue = $this->config->getAppValue($appName, $configName); } $this->writeMixedInOutputFormat($input, $output, $configValue); diff --git a/core/Command/Config/App/SetConfig.php b/core/Command/Config/App/SetConfig.php index 89a5f6ba5d1..ae6f24e71d4 100644 --- a/core/Command/Config/App/SetConfig.php +++ b/core/Command/Config/App/SetConfig.php @@ -1,8 +1,11 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Joas Schilling <coding@schilljs.com> + * @author Maxence Lange <maxence@artificial-owl.com> * * @license AGPL-3.0 * @@ -21,18 +24,21 @@ */ namespace OC\Core\Command\Config\App; -use OCP\IConfig; +use OC\AppConfig; +use OCP\Exceptions\AppConfigIncorrectTypeException; +use OCP\Exceptions\AppConfigUnknownKeyException; +use OCP\IAppConfig; 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 SetConfig extends Base { - protected IConfig $config; - - public function __construct(IConfig $config) { + public function __construct( + protected IAppConfig $appConfig, + ) { parent::__construct(); - $this->config = $config; } protected function configure() { @@ -58,6 +64,25 @@ class SetConfig extends Base { 'The new value of the config' ) ->addOption( + 'type', + null, + InputOption::VALUE_REQUIRED, + 'Value type [string, integer, float, boolean, array]', + 'string' + ) + ->addOption( + 'lazy', + null, + InputOption::VALUE_NEGATABLE, + 'Set value as lazy loaded', + ) + ->addOption( + 'sensitive', + null, + InputOption::VALUE_NEGATABLE, + 'Set value as sensitive', + ) + ->addOption( 'update-only', null, InputOption::VALUE_NONE, @@ -70,15 +95,176 @@ class SetConfig extends Base { $appName = $input->getArgument('app'); $configName = $input->getArgument('name'); - if (!in_array($configName, $this->config->getAppKeys($appName)) && $input->hasParameterOption('--update-only')) { - $output->writeln('<comment>Config value ' . $configName . ' for app ' . $appName . ' not updated, as it has not been set before.</comment>'); + if (!($this->appConfig instanceof AppConfig)) { + throw new \Exception('Only compatible with OC\AppConfig as it uses internal methods'); + } + + if ($input->hasParameterOption('--update-only') && !$this->appConfig->hasKey($appName, $configName)) { + $output->writeln( + '<comment>Config value ' . $configName . ' for app ' . $appName + . ' not updated, as it has not been set before.</comment>' + ); + return 1; } - $configValue = $input->getOption('value'); - $this->config->setAppValue($appName, $configName, $configValue); + $type = $typeString = null; + if ($input->hasParameterOption('--type')) { + $typeString = $input->getOption('type'); + $type = $this->appConfig->convertTypeToInt($typeString); + } + + /** + * If --Value is not specified, returns an exception if no value exists in database + * compare with current status in database and displays a reminder that this can break things. + * confirmation is required by admin, unless --no-interaction + */ + $updated = false; + if (!$input->hasParameterOption('--value')) { + if (!$input->getOption('lazy') && $this->appConfig->isLazy($appName, $configName) && $this->ask($input, $output, 'NOT LAZY')) { + $updated = $this->appConfig->updateLazy($appName, $configName, false); + } + if ($input->getOption('lazy') && !$this->appConfig->isLazy($appName, $configName) && $this->ask($input, $output, 'LAZY')) { + $updated = $this->appConfig->updateLazy($appName, $configName, true) || $updated; + } + if (!$input->getOption('sensitive') && $this->appConfig->isSensitive($appName, $configName) && $this->ask($input, $output, 'NOT SENSITIVE')) { + $updated = $this->appConfig->updateSensitive($appName, $configName, false) || $updated; + } + if ($input->getOption('sensitive') && !$this->appConfig->isSensitive($appName, $configName) && $this->ask($input, $output, 'SENSITIVE')) { + $updated = $this->appConfig->updateSensitive($appName, $configName, true) || $updated; + } + if ($type !== null && $type !== $this->appConfig->getValueType($appName, $configName) && $typeString !== null && $this->ask($input, $output, $typeString)) { + $updated = $this->appConfig->updateType($appName, $configName, $type) || $updated; + } + } else { + /** + * If --type is specified in the command line, we upgrade the type in database + * after a confirmation from admin. + * If not we get the type from current stored value or VALUE_MIXED as default. + */ + try { + $currType = $this->appConfig->getValueType($appName, $configName); + if ($type === null || $typeString === null || $type === $currType || !$this->ask($input, $output, $typeString)) { + $type = $currType; + } else { + $updated = $this->appConfig->updateType($appName, $configName, $type); + } + } catch (AppConfigUnknownKeyException) { + $type = $type ?? IAppConfig::VALUE_MIXED; + } + + /** + * if --lazy/--no-lazy option are set, compare with data stored in database. + * If no data in database, or identical, continue. + * If different, ask admin for confirmation. + */ + $lazy = $input->getOption('lazy'); + try { + $currLazy = $this->appConfig->isLazy($appName, $configName); + if ($lazy === null || $lazy === $currLazy || !$this->ask($input, $output, ($lazy) ? 'LAZY' : 'NOT LAZY')) { + $lazy = $currLazy; + } + } catch (AppConfigUnknownKeyException) { + $lazy = $lazy ?? false; + } + + /** + * same with sensitive status + */ + $sensitive = $input->getOption('sensitive'); + try { + $currSensitive = $this->appConfig->isLazy($appName, $configName); + if ($sensitive === null || $sensitive === $currSensitive || !$this->ask($input, $output, ($sensitive) ? 'LAZY' : 'NOT LAZY')) { + $sensitive = $currSensitive; + } + } catch (AppConfigUnknownKeyException) { + $sensitive = $sensitive ?? false; + } + + $value = (string)$input->getOption('value'); + + switch ($type) { + case IAppConfig::VALUE_MIXED: + $updated = $this->appConfig->setValueMixed($appName, $configName, $value, $lazy, $sensitive); + break; + + case IAppConfig::VALUE_STRING: + $updated = $this->appConfig->setValueString($appName, $configName, $value, $lazy, $sensitive); + break; + + case IAppConfig::VALUE_INT: + if ($value !== ((string) ((int) $value))) { + throw new AppConfigIncorrectTypeException('Value is not an integer'); + } + $updated = $this->appConfig->setValueInt($appName, $configName, (int)$value, $lazy, $sensitive); + break; + + case IAppConfig::VALUE_FLOAT: + if ($value !== ((string) ((float) $value))) { + throw new AppConfigIncorrectTypeException('Value is not a float'); + } + $updated = $this->appConfig->setValueFloat($appName, $configName, (float)$value, $lazy, $sensitive); + break; + + case IAppConfig::VALUE_BOOL: + if (in_array(strtolower($value), ['true', '1', 'on', 'yes'])) { + $valueBool = true; + } elseif (in_array(strtolower($value), ['false', '0', 'off', 'no'])) { + $valueBool = false; + } else { + throw new AppConfigIncorrectTypeException('Value is not a boolean, please use \'true\' or \'false\''); + } + $updated = $this->appConfig->setValueBool($appName, $configName, $valueBool, $lazy); + break; + + case IAppConfig::VALUE_ARRAY: + $valueArray = json_decode($value, true, flags: JSON_THROW_ON_ERROR); + $valueArray = (is_array($valueArray)) ? $valueArray : throw new AppConfigIncorrectTypeException('Value is not an array'); + $updated = $this->appConfig->setValueArray($appName, $configName, $valueArray, $lazy, $sensitive); + break; + } + } + + if ($updated) { + $current = $this->appConfig->getDetails($appName, $configName); + $output->writeln( + sprintf( + "<info>Config value '%s' for app '%s' is now set to '%s', stored as %s in %s</info>", + $configName, + $appName, + $current['value'], + $current['typeString'], + $current['lazy'] ? 'lazy cache' : 'fast cache' + ) + ); + } else { + $output->writeln('<info>Config value were not updated</info>'); + } - $output->writeln('<info>Config value ' . $configName . ' for app ' . $appName . ' set to ' . $configValue . '</info>'); return 0; } + + private function ask(InputInterface $input, OutputInterface $output, string $request): bool { + $helper = $this->getHelper('question'); + if ($input->getOption('no-interaction')) { + return true; + } + + $output->writeln(sprintf('You are about to set config value %s as <info>%s</info>', + '<info>' . $input->getArgument('app') . '</info>/<info>' . $input->getArgument('name') . '</info>', + strtoupper($request) + )); + $output->writeln(''); + $output->writeln('<comment>This might break thing, affect performance on your instance or its security!</comment>'); + + $result = (strtolower((string)$helper->ask( + $input, + $output, + new Question('<comment>Confirm this action by typing \'yes\'</comment>: '))) === 'yes'); + + $output->writeln(($result) ? 'done' : 'cancelled'); + $output->writeln(''); + + return $result; + } } diff --git a/core/Command/Config/Import.php b/core/Command/Config/Import.php index 227c909038c..b8431c8c295 100644 --- a/core/Command/Config/Import.php +++ b/core/Command/Config/Import.php @@ -35,11 +35,11 @@ use Symfony\Component\Console\Output\OutputInterface; class Import extends Command implements CompletionAwareInterface { protected array $validRootKeys = ['system', 'apps']; - protected IConfig $config; - public function __construct(IConfig $config) { + public function __construct( + protected IConfig $config, + ) { parent::__construct(); - $this->config = $config; } protected function configure() { diff --git a/core/Command/Config/ListConfigs.php b/core/Command/Config/ListConfigs.php index a0fa9a84ea8..51a17eb2097 100644 --- a/core/Command/Config/ListConfigs.php +++ b/core/Command/Config/ListConfigs.php @@ -33,13 +33,12 @@ use Symfony\Component\Console\Output\OutputInterface; class ListConfigs extends Base { protected string $defaultOutputFormat = self::OUTPUT_FORMAT_JSON_PRETTY; - protected SystemConfig $systemConfig; - protected IAppConfig $appConfig; - public function __construct(SystemConfig $systemConfig, IAppConfig $appConfig) { + public function __construct( + protected SystemConfig $systemConfig, + protected IAppConfig $appConfig, + ) { parent::__construct(); - $this->systemConfig = $systemConfig; - $this->appConfig = $appConfig; } protected function configure() { @@ -77,7 +76,7 @@ class ListConfigs extends Base { $configs = [ 'system' => $this->getSystemConfigs($noSensitiveValues), ]; - break; + break; case 'all': $apps = $this->appConfig->getApps(); @@ -88,13 +87,11 @@ class ListConfigs extends Base { foreach ($apps as $appName) { $configs['apps'][$appName] = $this->getAppConfigs($appName, $noSensitiveValues); } - break; + break; default: $configs = [ - 'apps' => [ - $app => $this->getAppConfigs($app, $noSensitiveValues), - ], + 'apps' => [$app => $this->getAppConfigs($app, $noSensitiveValues)], ]; } @@ -108,7 +105,7 @@ class ListConfigs extends Base { * @param bool $noSensitiveValues * @return array */ - protected function getSystemConfigs($noSensitiveValues) { + protected function getSystemConfigs(bool $noSensitiveValues): array { $keys = $this->systemConfig->getKeys(); $configs = []; @@ -134,7 +131,7 @@ class ListConfigs extends Base { * @param bool $noSensitiveValues * @return array */ - protected function getAppConfigs($app, $noSensitiveValues) { + protected function getAppConfigs(string $app, bool $noSensitiveValues) { if ($noSensitiveValues) { return $this->appConfig->getFilteredValues($app, false); } else { diff --git a/core/Command/Config/System/Base.php b/core/Command/Config/System/Base.php index 18bc9cb7ca0..09ec456f6a4 100644 --- a/core/Command/Config/System/Base.php +++ b/core/Command/Config/System/Base.php @@ -26,11 +26,10 @@ use OC\SystemConfig; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; abstract class Base extends \OC\Core\Command\Base { - protected SystemConfig $systemConfig; - - public function __construct(SystemConfig $systemConfig) { + public function __construct( + protected SystemConfig $systemConfig, + ) { parent::__construct(); - $this->systemConfig = $systemConfig; } /** diff --git a/core/Command/Config/System/DeleteConfig.php b/core/Command/Config/System/DeleteConfig.php index f4d49ba8f51..f6650e7d6d3 100644 --- a/core/Command/Config/System/DeleteConfig.php +++ b/core/Command/Config/System/DeleteConfig.php @@ -30,7 +30,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class DeleteConfig extends Base { - public function __construct(SystemConfig $systemConfig) { + public function __construct( + SystemConfig $systemConfig, + ) { parent::__construct($systemConfig); } diff --git a/core/Command/Config/System/GetConfig.php b/core/Command/Config/System/GetConfig.php index 01bbf82d5d1..ab5b884fda4 100644 --- a/core/Command/Config/System/GetConfig.php +++ b/core/Command/Config/System/GetConfig.php @@ -29,7 +29,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class GetConfig extends Base { - public function __construct(SystemConfig $systemConfig) { + public function __construct( + SystemConfig $systemConfig, + ) { parent::__construct($systemConfig); } diff --git a/core/Command/Config/System/SetConfig.php b/core/Command/Config/System/SetConfig.php index 01a1999bcf9..ba5265a84d7 100644 --- a/core/Command/Config/System/SetConfig.php +++ b/core/Command/Config/System/SetConfig.php @@ -32,7 +32,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class SetConfig extends Base { - public function __construct(SystemConfig $systemConfig) { + public function __construct( + SystemConfig $systemConfig, + ) { parent::__construct($systemConfig); } diff --git a/core/Command/Db/AddMissingColumns.php b/core/Command/Db/AddMissingColumns.php index acc05c3b7ff..07763c66154 100644 --- a/core/Command/Db/AddMissingColumns.php +++ b/core/Command/Db/AddMissingColumns.php @@ -28,13 +28,12 @@ namespace OC\Core\Command\Db; use OC\DB\Connection; use OC\DB\SchemaWrapper; -use OCP\IDBConnection; +use OCP\DB\Events\AddMissingColumnsEvent; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Class AddMissingColumns @@ -45,14 +44,11 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\Core\Command\Db */ class AddMissingColumns extends Command { - private Connection $connection; - private EventDispatcherInterface $dispatcher; - - public function __construct(Connection $connection, EventDispatcherInterface $dispatcher) { + public function __construct( + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { parent::__construct(); - - $this->connection = $connection; - $this->dispatcher = $dispatcher; } protected function configure() { @@ -63,46 +59,38 @@ class AddMissingColumns extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->addCoreColumns($output, $input->getOption('dry-run')); + $dryRun = $input->getOption('dry-run'); // Dispatch event so apps can also update columns if needed - $event = new GenericEvent($output); - $this->dispatcher->dispatch(IDBConnection::ADD_MISSING_COLUMNS_EVENT, $event); - return 0; - } - - /** - * add missing indices to the share table - * - * @param OutputInterface $output - * @param bool $dryRun If true, will return the sql queries instead of running them. - * @throws \Doctrine\DBAL\Schema\SchemaException - */ - private function addCoreColumns(OutputInterface $output, bool $dryRun): void { - $output->writeln('<info>Check columns of the comments table.</info>'); - - $schema = new SchemaWrapper($this->connection); + $event = new AddMissingColumnsEvent(); + $this->dispatcher->dispatchTyped($event); + $missingColumns = $event->getMissingColumns(); $updated = false; - if ($schema->hasTable('comments')) { - $table = $schema->getTable('comments'); - if (!$table->hasColumn('reference_id')) { - $output->writeln('<info>Adding additional reference_id column to the comments table, this can take some time...</info>'); - $table->addColumn('reference_id', 'string', [ - 'notnull' => false, - 'length' => 64, - ]); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); + if (!empty($missingColumns)) { + $schema = new SchemaWrapper($this->connection); + + foreach ($missingColumns as $missingColumn) { + if ($schema->hasTable($missingColumn['tableName'])) { + $table = $schema->getTable($missingColumn['tableName']); + if (!$table->hasColumn($missingColumn['columnName'])) { + $output->writeln('<info>Adding additional ' . $missingColumn['columnName'] . ' column to the ' . $missingColumn['tableName'] . ' table, this can take some time...</info>'); + $table->addColumn($missingColumn['columnName'], $missingColumn['typeName'], $missingColumn['options']); + $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); + if ($dryRun && $sqlQueries !== null) { + $output->writeln($sqlQueries); + } + $updated = true; + $output->writeln('<info>' . $missingColumn['tableName'] . ' table updated successfully.</info>'); + } } - $updated = true; - $output->writeln('<info>Comments table updated successfully.</info>'); } } if (!$updated) { $output->writeln('<info>Done.</info>'); } + + return 0; } } diff --git a/core/Command/Db/AddMissingIndices.php b/core/Command/Db/AddMissingIndices.php index 5799a462ffa..1e10b6152ce 100644 --- a/core/Command/Db/AddMissingIndices.php +++ b/core/Command/Db/AddMissingIndices.php @@ -33,16 +33,14 @@ declare(strict_types=1); */ namespace OC\Core\Command\Db; -use Doctrine\DBAL\Platforms\PostgreSQL94Platform; use OC\DB\Connection; use OC\DB\SchemaWrapper; -use OCP\IDBConnection; +use OCP\DB\Events\AddMissingIndicesEvent; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Class AddMissingIndices @@ -53,14 +51,11 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\Core\Command\Db */ class AddMissingIndices extends Command { - private Connection $connection; - private EventDispatcherInterface $dispatcher; - - public function __construct(Connection $connection, EventDispatcherInterface $dispatcher) { + public function __construct( + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { parent::__construct(); - - $this->connection = $connection; - $this->dispatcher = $dispatcher; } protected function configure() { @@ -71,394 +66,94 @@ class AddMissingIndices extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->addCoreIndexes($output, $input->getOption('dry-run')); + $dryRun = $input->getOption('dry-run'); // Dispatch event so apps can also update indexes if needed - $event = new GenericEvent($output); - $this->dispatcher->dispatch(IDBConnection::ADD_MISSING_INDEXES_EVENT, $event); - return 0; - } - - /** - * add missing indices to the share table - * - * @param OutputInterface $output - * @param bool $dryRun If true, will return the sql queries instead of running them. - * @throws \Doctrine\DBAL\Schema\SchemaException - */ - private function addCoreIndexes(OutputInterface $output, bool $dryRun): void { - $output->writeln('<info>Check indices of the share table.</info>'); - - $schema = new SchemaWrapper($this->connection); - $updated = false; - - if ($schema->hasTable('share')) { - $table = $schema->getTable('share'); - if (!$table->hasIndex('share_with_index')) { - $output->writeln('<info>Adding additional share_with index to the share table, this can take some time...</info>'); - $table->addIndex(['share_with'], 'share_with_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - - if (!$table->hasIndex('parent_index')) { - $output->writeln('<info>Adding additional parent index to the share table, this can take some time...</info>'); - $table->addIndex(['parent'], 'parent_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - - if (!$table->hasIndex('owner_index')) { - $output->writeln('<info>Adding additional owner index to the share table, this can take some time...</info>'); - $table->addIndex(['uid_owner'], 'owner_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - - if (!$table->hasIndex('initiator_index')) { - $output->writeln('<info>Adding additional initiator index to the share table, this can take some time...</info>'); - $table->addIndex(['uid_initiator'], 'initiator_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Share table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the filecache table.</info>'); - if ($schema->hasTable('filecache')) { - $table = $schema->getTable('filecache'); - if (!$table->hasIndex('fs_mtime')) { - $output->writeln('<info>Adding additional mtime index to the filecache table, this can take some time...</info>'); - $table->addIndex(['mtime'], 'fs_mtime'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - if (!$table->hasIndex('fs_size')) { - $output->writeln('<info>Adding additional size index to the filecache table, this can take some time...</info>'); - $table->addIndex(['size'], 'fs_size'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - if (!$table->hasIndex('fs_id_storage_size')) { - $output->writeln('<info>Adding additional size index to the filecache table, this can take some time...</info>'); - $table->addIndex(['fileid', 'storage', 'size'], 'fs_id_storage_size'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - if (!$table->hasIndex('fs_storage_path_prefix') && !$schema->getDatabasePlatform() instanceof PostgreSQL94Platform) { - $output->writeln('<info>Adding additional path index to the filecache table, this can take some time...</info>'); - $table->addIndex(['storage', 'path'], 'fs_storage_path_prefix', [], ['lengths' => [null, 64]]); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Filecache table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the twofactor_providers table.</info>'); - if ($schema->hasTable('twofactor_providers')) { - $table = $schema->getTable('twofactor_providers'); - if (!$table->hasIndex('twofactor_providers_uid')) { - $output->writeln('<info>Adding additional twofactor_providers_uid index to the twofactor_providers table, this can take some time...</info>'); - $table->addIndex(['uid'], 'twofactor_providers_uid'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>Twofactor_providers table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the login_flow_v2 table.</info>'); - if ($schema->hasTable('login_flow_v2')) { - $table = $schema->getTable('login_flow_v2'); - if (!$table->hasIndex('poll_token')) { - $output->writeln('<info>Adding additional indeces to the login_flow_v2 table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - $columns = $index->getColumns(); - if ($columns === ['poll_token'] || - $columns === ['login_token'] || - $columns === ['timestamp']) { - $table->dropIndex($index->getName()); - } - } - - $table->addUniqueIndex(['poll_token'], 'poll_token'); - $table->addUniqueIndex(['login_token'], 'login_token'); - $table->addIndex(['timestamp'], 'timestamp'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>login_flow_v2 table updated successfully.</info>'); - } - } + $event = new AddMissingIndicesEvent(); + $this->dispatcher->dispatchTyped($event); + + $missingIndices = $event->getMissingIndices(); + $toReplaceIndices = $event->getIndicesToReplace(); + + if ($missingIndices !== [] || $toReplaceIndices !== []) { + $schema = new SchemaWrapper($this->connection); + + foreach ($missingIndices as $missingIndex) { + if ($schema->hasTable($missingIndex['tableName'])) { + $table = $schema->getTable($missingIndex['tableName']); + if (!$table->hasIndex($missingIndex['indexName'])) { + $output->writeln('<info>Adding additional ' . $missingIndex['indexName'] . ' index to the ' . $table->getName() . ' table, this can take some time...</info>'); + + if ($missingIndex['dropUnnamedIndex']) { + foreach ($table->getIndexes() as $index) { + $columns = $index->getColumns(); + if ($columns === $missingIndex['columns']) { + $table->dropIndex($index->getName()); + } + } + } - $output->writeln('<info>Check indices of the whats_new table.</info>'); - if ($schema->hasTable('whats_new')) { - $table = $schema->getTable('whats_new'); - if (!$table->hasIndex('version')) { - $output->writeln('<info>Adding version index to the whats_new table, this can take some time...</info>'); + if ($missingIndex['uniqueIndex']) { + $table->addUniqueIndex($missingIndex['columns'], $missingIndex['indexName'], $missingIndex['options']); + } else { + $table->addIndex($missingIndex['columns'], $missingIndex['indexName'], [], $missingIndex['options']); + } - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['version']) { - $table->dropIndex($index->getName()); + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); + } + $output->writeln('<info>' . $table->getName() . ' table updated successfully.</info>'); } } - - $table->addUniqueIndex(['version'], 'version'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>whats_new table updated successfully.</info>'); } - } - - $output->writeln('<info>Check indices of the cards table.</info>'); - $cardsUpdated = false; - if ($schema->hasTable('cards')) { - $table = $schema->getTable('cards'); - if ($table->hasIndex('addressbookid_uri_index')) { - if ($table->hasIndex('cards_abiduri')) { - $table->dropIndex('addressbookid_uri_index'); - } else { - $output->writeln('<info>Renaming addressbookid_uri_index index to cards_abiduri in the cards table, this can take some time...</info>'); + foreach ($toReplaceIndices as $toReplaceIndex) { + if ($schema->hasTable($toReplaceIndex['tableName'])) { + $table = $schema->getTable($toReplaceIndex['tableName']); - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid', 'uri']) { - $table->renameIndex('addressbookid_uri_index', 'cards_abiduri'); + $allOldIndicesExists = true; + foreach ($toReplaceIndex['oldIndexNames'] as $oldIndexName) { + if (!$table->hasIndex($oldIndexName)) { + $allOldIndicesExists = false; } } - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $cardsUpdated = true; - } - - if (!$table->hasIndex('cards_abid')) { - $output->writeln('<info>Adding cards_abid index to the cards table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid']) { - $table->dropIndex($index->getName()); + if (!$allOldIndicesExists) { + continue; } - } - $table->addIndex(['addressbookid'], 'cards_abid'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $cardsUpdated = true; - } + $output->writeln('<info>Adding additional ' . $toReplaceIndex['newIndexName'] . ' index to the ' . $table->getName() . ' table, this can take some time...</info>'); - if (!$table->hasIndex('cards_abiduri')) { - $output->writeln('<info>Adding cards_abiduri index to the cards table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid', 'uri']) { - $table->dropIndex($index->getName()); + if ($toReplaceIndex['uniqueIndex']) { + $table->addUniqueIndex($toReplaceIndex['columns'], $toReplaceIndex['newIndexName'], $toReplaceIndex['options']); + } else { + $table->addIndex($toReplaceIndex['columns'], $toReplaceIndex['newIndexName'], [], $toReplaceIndex['options']); } - } - $table->addIndex(['addressbookid', 'uri'], 'cards_abiduri'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $cardsUpdated = true; - } - - if ($cardsUpdated) { - $updated = true; - $output->writeln('<info>cards table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the cards_properties table.</info>'); - if ($schema->hasTable('cards_properties')) { - $table = $schema->getTable('cards_properties'); - if (!$table->hasIndex('cards_prop_abid')) { - $output->writeln('<info>Adding cards_prop_abid index to the cards_properties table, this can take some time...</info>'); - - foreach ($table->getIndexes() as $index) { - if ($index->getColumns() === ['addressbookid']) { - $table->dropIndex($index->getName()); + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); } - } - - $table->addIndex(['addressbookid'], 'cards_prop_abid'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>cards_properties table updated successfully.</info>'); - } - } - $output->writeln('<info>Check indices of the calendarobjects_props table.</info>'); - if ($schema->hasTable('calendarobjects_props')) { - $table = $schema->getTable('calendarobjects_props'); - if (!$table->hasIndex('calendarobject_calid_index')) { - $output->writeln('<info>Adding calendarobject_calid_index index to the calendarobjects_props table, this can take some time...</info>'); - - $table->addIndex(['calendarid', 'calendartype'], 'calendarobject_calid_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>calendarobjects_props table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the schedulingobjects table.</info>'); - if ($schema->hasTable('schedulingobjects')) { - $table = $schema->getTable('schedulingobjects'); - if (!$table->hasIndex('schedulobj_principuri_index')) { - $output->writeln('<info>Adding schedulobj_principuri_index index to the schedulingobjects table, this can take some time...</info>'); - - $table->addIndex(['principaluri'], 'schedulobj_principuri_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>schedulingobjects table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the oc_properties table.</info>'); - if ($schema->hasTable('properties')) { - $table = $schema->getTable('properties'); - $propertiesUpdated = false; - - if (!$table->hasIndex('properties_path_index')) { - $output->writeln('<info>Adding properties_path_index index to the oc_properties table, this can take some time...</info>'); - - $table->addIndex(['userid', 'propertypath'], 'properties_path_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $propertiesUpdated = true; - } - if (!$table->hasIndex('properties_pathonly_index')) { - $output->writeln('<info>Adding properties_pathonly_index index to the oc_properties table, this can take some time...</info>'); - - $table->addIndex(['propertypath'], 'properties_pathonly_index'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $propertiesUpdated = true; - } - - if ($propertiesUpdated) { - $updated = true; - $output->writeln('<info>oc_properties table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the oc_jobs table.</info>'); - if ($schema->hasTable('jobs')) { - $table = $schema->getTable('jobs'); - if (!$table->hasIndex('job_lastcheck_reserved')) { - $output->writeln('<info>Adding job_lastcheck_reserved index to the oc_jobs table, this can take some time...</info>'); + foreach ($toReplaceIndex['oldIndexNames'] as $oldIndexName) { + $output->writeln('<info>Removing ' . $oldIndexName . ' index from the ' . $table->getName() . ' table</info>'); + $table->dropIndex($oldIndexName); + } - $table->addIndex(['last_checked', 'reserved_at'], 'job_lastcheck_reserved'); - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); + } + $output->writeln('<info>' . $table->getName() . ' table updated successfully.</info>'); } - $updated = true; - $output->writeln('<info>oc_properties table updated successfully.</info>'); } - } - $output->writeln('<info>Check indices of the oc_direct_edit table.</info>'); - if ($schema->hasTable('direct_edit')) { - $table = $schema->getTable('direct_edit'); - if (!$table->hasIndex('direct_edit_timestamp')) { - $output->writeln('<info>Adding direct_edit_timestamp index to the oc_direct_edit table, this can take some time...</info>'); - - $table->addIndex(['timestamp'], 'direct_edit_timestamp'); + if ($dryRun) { $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { + if ($sqlQueries !== null) { $output->writeln($sqlQueries); } - $updated = true; - $output->writeln('<info>oc_direct_edit table updated successfully.</info>'); - } - } - - $output->writeln('<info>Check indices of the oc_preferences table.</info>'); - if ($schema->hasTable('preferences')) { - $table = $schema->getTable('preferences'); - if (!$table->hasIndex('preferences_app_key')) { - $output->writeln('<info>Adding preferences_app_key index to the oc_preferences table, this can take some time...</info>'); - - $table->addIndex(['appid', 'configkey'], 'preferences_app_key'); - $this->connection->migrateToSchema($schema->getWrappedSchema()); - $updated = true; - $output->writeln('<info>oc_properties table updated successfully.</info>'); } } - $output->writeln('<info>Check indices of the oc_mounts table.</info>'); - if ($schema->hasTable('mounts')) { - $table = $schema->getTable('mounts'); - if (!$table->hasIndex('mounts_class_index')) { - $output->writeln('<info>Adding mounts_class_index index to the oc_mounts table, this can take some time...</info>'); - - $table->addIndex(['mount_provider_class'], 'mounts_class_index'); - $this->connection->migrateToSchema($schema->getWrappedSchema()); - $updated = true; - $output->writeln('<info>oc_mounts table updated successfully.</info>'); - } - } - - if (!$updated) { - $output->writeln('<info>Done.</info>'); - } + return 0; } } diff --git a/core/Command/Db/AddMissingPrimaryKeys.php b/core/Command/Db/AddMissingPrimaryKeys.php index 8262cf37e77..658eb0b0f5a 100644 --- a/core/Command/Db/AddMissingPrimaryKeys.php +++ b/core/Command/Db/AddMissingPrimaryKeys.php @@ -28,13 +28,12 @@ namespace OC\Core\Command\Db; use OC\DB\Connection; use OC\DB\SchemaWrapper; -use OCP\IDBConnection; +use OCP\DB\Events\AddMissingPrimaryKeyEvent; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; /** * Class AddMissingPrimaryKeys @@ -45,14 +44,11 @@ use Symfony\Component\EventDispatcher\GenericEvent; * @package OC\Core\Command\Db */ class AddMissingPrimaryKeys extends Command { - private Connection $connection; - private EventDispatcherInterface $dispatcher; - - public function __construct(Connection $connection, EventDispatcherInterface $dispatcher) { + public function __construct( + private Connection $connection, + private IEventDispatcher $dispatcher, + ) { parent::__construct(); - - $this->connection = $connection; - $this->dispatcher = $dispatcher; } protected function configure() { @@ -63,131 +59,44 @@ class AddMissingPrimaryKeys extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->addCorePrimaryKeys($output, $input->getOption('dry-run')); + $dryRun = $input->getOption('dry-run'); // Dispatch event so apps can also update indexes if needed - $event = new GenericEvent($output); - $this->dispatcher->dispatch(IDBConnection::ADD_MISSING_PRIMARY_KEYS_EVENT, $event); - return 0; - } - - /** - * add missing indices to the share table - * - * @param OutputInterface $output - * @param bool $dryRun If true, will return the sql queries instead of running them. - * @throws \Doctrine\DBAL\Schema\SchemaException - */ - private function addCorePrimaryKeys(OutputInterface $output, bool $dryRun): void { - $output->writeln('<info>Check primary keys.</info>'); - - $schema = new SchemaWrapper($this->connection); + $event = new AddMissingPrimaryKeyEvent(); + $this->dispatcher->dispatchTyped($event); + $missingKeys = $event->getMissingPrimaryKeys(); $updated = false; - if ($schema->hasTable('federated_reshares')) { - $table = $schema->getTable('federated_reshares'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the federated_reshares table, this can take some time...</info>'); - $table->setPrimaryKey(['share_id'], 'federated_res_pk'); - if ($table->hasIndex('share_id_index')) { - $table->dropIndex('share_id_index'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>federated_reshares table updated successfully.</info>'); - } - } - - if ($schema->hasTable('systemtag_object_mapping')) { - $table = $schema->getTable('systemtag_object_mapping'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the systemtag_object_mapping table, this can take some time...</info>'); - $table->setPrimaryKey(['objecttype', 'objectid', 'systemtagid'], 'som_pk'); - if ($table->hasIndex('mapping')) { - $table->dropIndex('mapping'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>systemtag_object_mapping table updated successfully.</info>'); - } - } + if (!empty($missingKeys)) { + $schema = new SchemaWrapper($this->connection); - if ($schema->hasTable('comments_read_markers')) { - $table = $schema->getTable('comments_read_markers'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the comments_read_markers table, this can take some time...</info>'); - $table->setPrimaryKey(['user_id', 'object_type', 'object_id'], 'crm_pk'); - if ($table->hasIndex('comments_marker_index')) { - $table->dropIndex('comments_marker_index'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>comments_read_markers table updated successfully.</info>'); - } - } + foreach ($missingKeys as $missingKey) { + if ($schema->hasTable($missingKey['tableName'])) { + $table = $schema->getTable($missingKey['tableName']); + if (!$table->hasPrimaryKey()) { + $output->writeln('<info>Adding primary key to the ' . $missingKey['tableName'] . ' table, this can take some time...</info>'); + $table->setPrimaryKey($missingKey['columns'], $missingKey['primaryKeyName']); - if ($schema->hasTable('collres_resources')) { - $table = $schema->getTable('collres_resources'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the collres_resources table, this can take some time...</info>'); - $table->setPrimaryKey(['collection_id', 'resource_type', 'resource_id'], 'crr_pk'); - if ($table->hasIndex('collres_unique_res')) { - $table->dropIndex('collres_unique_res'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>collres_resources table updated successfully.</info>'); - } - } + if ($missingKey['formerIndex'] && $table->hasIndex($missingKey['formerIndex'])) { + $table->dropIndex($missingKey['formerIndex']); + } - if ($schema->hasTable('collres_accesscache')) { - $table = $schema->getTable('collres_accesscache'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the collres_accesscache table, this can take some time...</info>'); - $table->setPrimaryKey(['user_id', 'collection_id', 'resource_type', 'resource_id'], 'cra_pk'); - if ($table->hasIndex('collres_unique_user')) { - $table->dropIndex('collres_unique_user'); - } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>collres_accesscache table updated successfully.</info>'); - } - } + $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); + if ($dryRun && $sqlQueries !== null) { + $output->writeln($sqlQueries); + } - if ($schema->hasTable('filecache_extended')) { - $table = $schema->getTable('filecache_extended'); - if (!$table->hasPrimaryKey()) { - $output->writeln('<info>Adding primary key to the filecache_extended table, this can take some time...</info>'); - $table->setPrimaryKey(['fileid'], 'fce_pk'); - if ($table->hasIndex('fce_fileid_idx')) { - $table->dropIndex('fce_fileid_idx'); + $updated = true; + $output->writeln('<info>' . $missingKey['tableName'] . ' table updated successfully.</info>'); + } } - $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); - if ($dryRun && $sqlQueries !== null) { - $output->writeln($sqlQueries); - } - $updated = true; - $output->writeln('<info>filecache_extended table updated successfully.</info>'); } } if (!$updated) { $output->writeln('<info>Done.</info>'); } + + return 0; } } diff --git a/core/Command/Db/ConvertFilecacheBigInt.php b/core/Command/Db/ConvertFilecacheBigInt.php index f12ae15f0b3..44cd81cd7eb 100644 --- a/core/Command/Db/ConvertFilecacheBigInt.php +++ b/core/Command/Db/ConvertFilecacheBigInt.php @@ -33,19 +33,18 @@ namespace OC\Core\Command\Db; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Types\Type; -use OCP\DB\Types; use OC\DB\Connection; use OC\DB\SchemaWrapper; +use OCP\DB\Types; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class ConvertFilecacheBigInt extends Command { - private Connection $connection; - - public function __construct(Connection $connection) { - $this->connection = $connection; + public function __construct( + private Connection $connection, + ) { parent::__construct(); } @@ -55,8 +54,10 @@ class ConvertFilecacheBigInt extends Command { ->setDescription('Convert the ID columns of the filecache to BigInt'); } - protected function getColumnsByTable() { - // also update in CheckSetupController::hasBigIntConversionPendingColumns() + /** + * @return array<string,string[]> + */ + public static function getColumnsByTable(): array { return [ 'activity' => ['activity_id', 'object_id'], 'activity_mq' => ['mail_id'], @@ -67,6 +68,7 @@ class ConvertFilecacheBigInt extends Command { 'filecache_extended' => ['fileid'], 'files_trash' => ['auto_id'], 'file_locks' => ['id'], + 'file_metadata' => ['id'], 'jobs' => ['id'], 'mimetypes' => ['id'], 'mounts' => ['id', 'storage_id', 'root_id', 'mount_id'], @@ -80,7 +82,7 @@ class ConvertFilecacheBigInt extends Command { $isSqlite = $this->connection->getDatabasePlatform() instanceof SqlitePlatform; $updates = []; - $tables = $this->getColumnsByTable(); + $tables = static::getColumnsByTable(); foreach ($tables as $tableName => $columns) { if (!$schema->hasTable($tableName)) { continue; diff --git a/core/Command/Db/ConvertMysqlToMB4.php b/core/Command/Db/ConvertMysqlToMB4.php index 19a9532d910..a66fb46fc51 100644 --- a/core/Command/Db/ConvertMysqlToMB4.php +++ b/core/Command/Db/ConvertMysqlToMB4.php @@ -37,21 +37,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ConvertMysqlToMB4 extends Command { - private IConfig $config; - private IDBConnection $connection; - private IURLGenerator $urlGenerator; - private LoggerInterface $logger; - public function __construct( - IConfig $config, - IDBConnection $connection, - IURLGenerator $urlGenerator, - LoggerInterface $logger + private IConfig $config, + private IDBConnection $connection, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, ) { - $this->config = $config; - $this->connection = $connection; - $this->urlGenerator = $urlGenerator; - $this->logger = $logger; parent::__construct(); } diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index f7638e3024f..db618e938c0 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -35,11 +35,11 @@ namespace OC\Core\Command\Db; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Schema\AbstractAsset; use Doctrine\DBAL\Schema\Table; -use OCP\DB\Types; use OC\DB\Connection; use OC\DB\ConnectionFactory; use OC\DB\MigrationService; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\Types; use OCP\IConfig; use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; @@ -56,13 +56,12 @@ use function preg_match; use function preg_quote; class ConvertType extends Command implements CompletionAwareInterface { - protected IConfig $config; - protected ConnectionFactory $connectionFactory; - protected array $columnTypes; + protected array $columnTypes = []; - public function __construct(IConfig $config, ConnectionFactory $connectionFactory) { - $this->config = $config; - $this->connectionFactory = $connectionFactory; + public function __construct( + protected IConfig $config, + protected ConnectionFactory $connectionFactory, + ) { parent::__construct(); } @@ -200,7 +199,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $output->writeln('<comment>The following tables will not be converted:</comment>'); $output->writeln($extraFromTables); if (!$input->getOption('all-apps')) { - $output->writeln('<comment>Please note that tables belonging to available but currently not installed apps</comment>'); + $output->writeln('<comment>Please note that tables belonging to disabled (but not removed) apps</comment>'); $output->writeln('<comment>can be included by specifying the --all-apps option.</comment>'); } diff --git a/core/Command/Db/Migrations/ExecuteCommand.php b/core/Command/Db/Migrations/ExecuteCommand.php index e87e133fa31..c75b575ab6c 100644 --- a/core/Command/Db/Migrations/ExecuteCommand.php +++ b/core/Command/Db/Migrations/ExecuteCommand.php @@ -35,13 +35,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ExecuteCommand extends Command implements CompletionAwareInterface { - private Connection $connection; - private IConfig $config; - - public function __construct(Connection $connection, IConfig $config) { - $this->connection = $connection; - $this->config = $config; - + public function __construct( + private Connection $connection, + private IConfig $config, + ) { parent::__construct(); } diff --git a/core/Command/Db/Migrations/GenerateCommand.php b/core/Command/Db/Migrations/GenerateCommand.php index 6c11c7705d2..47f65b5a11b 100644 --- a/core/Command/Db/Migrations/GenerateCommand.php +++ b/core/Command/Db/Migrations/GenerateCommand.php @@ -45,9 +45,9 @@ class GenerateCommand extends Command implements CompletionAwareInterface { declare(strict_types=1); /** - * @copyright Copyright (c) {{year}} Your name <your@email.com> + * @copyright Copyright (c) {{year}} FIXME Your name <your@email.com> * - * @author Your name <your@email.com> + * FIXME @author Your name <your@email.com> * * @license GNU AGPL version 3 or any later version * @@ -74,13 +74,13 @@ use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; /** - * Auto-generated migration step: Please modify to your needs! + * FIXME Auto-generated migration step: Please modify to your needs! */ class {{classname}} extends SimpleMigrationStep { /** * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { @@ -88,7 +88,7 @@ class {{classname}} extends SimpleMigrationStep { /** * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options * @return null|ISchemaWrapper */ @@ -98,7 +98,7 @@ class {{classname}} extends SimpleMigrationStep { /** * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { @@ -235,7 +235,7 @@ class {{classname}} extends SimpleMigrationStep { $path = $dir . '/' . $className . '.php'; if (file_put_contents($path, $code) === false) { - throw new RuntimeException('Failed to generate new migration step.'); + throw new RuntimeException('Failed to generate new migration step. Could not write to ' . $path); } return $path; diff --git a/core/Command/Db/Migrations/MigrateCommand.php b/core/Command/Db/Migrations/MigrateCommand.php index f0f35716997..3e11b32665a 100644 --- a/core/Command/Db/Migrations/MigrateCommand.php +++ b/core/Command/Db/Migrations/MigrateCommand.php @@ -33,10 +33,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class MigrateCommand extends Command implements CompletionAwareInterface { - private Connection $connection; - - public function __construct(Connection $connection) { - $this->connection = $connection; + public function __construct( + private Connection $connection, + ) { parent::__construct(); } diff --git a/core/Command/Db/Migrations/StatusCommand.php b/core/Command/Db/Migrations/StatusCommand.php index 725ee075215..52bc51a169f 100644 --- a/core/Command/Db/Migrations/StatusCommand.php +++ b/core/Command/Db/Migrations/StatusCommand.php @@ -34,10 +34,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class StatusCommand extends Command implements CompletionAwareInterface { - private Connection $connection; - - public function __construct(Connection $connection) { - $this->connection = $connection; + public function __construct( + private Connection $connection, + ) { parent::__construct(); } diff --git a/core/Command/Encryption/ChangeKeyStorageRoot.php b/core/Command/Encryption/ChangeKeyStorageRoot.php index 6ae59421a69..96884de25e9 100644 --- a/core/Command/Encryption/ChangeKeyStorageRoot.php +++ b/core/Command/Encryption/ChangeKeyStorageRoot.php @@ -40,19 +40,14 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class ChangeKeyStorageRoot extends Command { - protected View $rootView; - protected IUserManager $userManager; - protected IConfig $config; - protected Util $util; - protected QuestionHelper $questionHelper; - - public function __construct(View $view, IUserManager $userManager, IConfig $config, Util $util, QuestionHelper $questionHelper) { + public function __construct( + protected View $rootView, + protected IUserManager $userManager, + protected IConfig $config, + protected Util $util, + protected QuestionHelper $questionHelper, + ) { parent::__construct(); - $this->rootView = $view; - $this->userManager = $userManager; - $this->config = $config; - $this->util = $util; - $this->questionHelper = $questionHelper; } protected function configure() { diff --git a/core/Command/Encryption/DecryptAll.php b/core/Command/Encryption/DecryptAll.php index ce17f787abd..137b12141f7 100644 --- a/core/Command/Encryption/DecryptAll.php +++ b/core/Command/Encryption/DecryptAll.php @@ -41,28 +41,17 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class DecryptAll extends Command { - protected IManager $encryptionManager; - protected IAppManager $appManager; - protected IConfig $config; - protected QuestionHelper $questionHelper; - protected bool $wasTrashbinEnabled; - protected bool $wasMaintenanceModeEnabled; - protected \OC\Encryption\DecryptAll $decryptAll; + protected bool $wasTrashbinEnabled = false; + protected bool $wasMaintenanceModeEnabled = false; public function __construct( - IManager $encryptionManager, - IAppManager $appManager, - IConfig $config, - \OC\Encryption\DecryptAll $decryptAll, - QuestionHelper $questionHelper + protected IManager $encryptionManager, + protected IAppManager $appManager, + protected IConfig $config, + protected \OC\Encryption\DecryptAll $decryptAll, + protected QuestionHelper $questionHelper, ) { parent::__construct(); - - $this->appManager = $appManager; - $this->encryptionManager = $encryptionManager; - $this->config = $config; - $this->decryptAll = $decryptAll; - $this->questionHelper = $questionHelper; } /** diff --git a/core/Command/Encryption/Disable.php b/core/Command/Encryption/Disable.php index 446601a1b4f..4c6e3431f93 100644 --- a/core/Command/Encryption/Disable.php +++ b/core/Command/Encryption/Disable.php @@ -27,11 +27,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Disable extends Command { - protected IConfig $config; - - public function __construct(IConfig $config) { + public function __construct( + protected IConfig $config, + ) { parent::__construct(); - $this->config = $config; } protected function configure() { diff --git a/core/Command/Encryption/Enable.php b/core/Command/Encryption/Enable.php index 9d680144e60..2cbb315283e 100644 --- a/core/Command/Encryption/Enable.php +++ b/core/Command/Encryption/Enable.php @@ -29,14 +29,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Enable extends Command { - protected IConfig $config; - protected IManager $encryptionManager; - - public function __construct(IConfig $config, IManager $encryptionManager) { + public function __construct( + protected IConfig $config, + protected IManager $encryptionManager, + ) { parent::__construct(); - - $this->encryptionManager = $encryptionManager; - $this->config = $config; } protected function configure() { @@ -59,18 +56,18 @@ class Enable extends Command { if (empty($modules)) { $output->writeln('<error>No encryption module is loaded</error>'); return 1; - } else { - $defaultModule = $this->config->getAppValue('core', 'default_encryption_module', null); - if ($defaultModule === null) { - $output->writeln('<error>No default module is set</error>'); - return 1; - } elseif (!isset($modules[$defaultModule])) { - $output->writeln('<error>The current default module does not exist: ' . $defaultModule . '</error>'); - return 1; - } else { - $output->writeln('Default module: ' . $defaultModule); - } } + $defaultModule = $this->config->getAppValue('core', 'default_encryption_module', null); + if ($defaultModule === null) { + $output->writeln('<error>No default module is set</error>'); + return 1; + } + if (!isset($modules[$defaultModule])) { + $output->writeln('<error>The current default module does not exist: ' . $defaultModule . '</error>'); + return 1; + } + $output->writeln('Default module: ' . $defaultModule); + return 0; } } diff --git a/core/Command/Encryption/EncryptAll.php b/core/Command/Encryption/EncryptAll.php index 11e33ae9e2e..cf4ee749791 100644 --- a/core/Command/Encryption/EncryptAll.php +++ b/core/Command/Encryption/EncryptAll.php @@ -36,24 +36,16 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class EncryptAll extends Command { - protected IManager $encryptionManager; - protected IAppManager $appManager; - protected IConfig $config; - protected QuestionHelper $questionHelper; protected bool $wasTrashbinEnabled = false; - protected bool $wasMaintenanceModeEnabled; + protected bool $wasMaintenanceModeEnabled = false; public function __construct( - IManager $encryptionManager, - IAppManager $appManager, - IConfig $config, - QuestionHelper $questionHelper + protected IManager $encryptionManager, + protected IAppManager $appManager, + protected IConfig $config, + protected QuestionHelper $questionHelper, ) { parent::__construct(); - $this->appManager = $appManager; - $this->encryptionManager = $encryptionManager; - $this->config = $config; - $this->questionHelper = $questionHelper; } /** diff --git a/core/Command/Encryption/ListModules.php b/core/Command/Encryption/ListModules.php index 88ad9875073..46be88864a7 100644 --- a/core/Command/Encryption/ListModules.php +++ b/core/Command/Encryption/ListModules.php @@ -30,16 +30,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ListModules extends Base { - protected IManager $encryptionManager; - protected IConfig $config; - public function __construct( - IManager $encryptionManager, - IConfig $config + protected IManager $encryptionManager, + protected IConfig $config, ) { parent::__construct(); - $this->encryptionManager = $encryptionManager; - $this->config = $config; } protected function configure() { diff --git a/core/Command/Encryption/MigrateKeyStorage.php b/core/Command/Encryption/MigrateKeyStorage.php index 8d9c7910769..2441aa9cc1a 100644 --- a/core/Command/Encryption/MigrateKeyStorage.php +++ b/core/Command/Encryption/MigrateKeyStorage.php @@ -33,25 +33,18 @@ use OCP\IUserManager; use OCP\Security\ICrypto; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class MigrateKeyStorage extends Command { - protected View $rootView; - protected IUserManager $userManager; - protected IConfig $config; - protected Util $util; - protected QuestionHelper $questionHelper; - private ICrypto $crypto; - - public function __construct(View $view, IUserManager $userManager, IConfig $config, Util $util, ICrypto $crypto) { + public function __construct( + protected View $rootView, + protected IUserManager $userManager, + protected IConfig $config, + protected Util $util, + private ICrypto $crypto, + ) { parent::__construct(); - $this->rootView = $view; - $this->userManager = $userManager; - $this->config = $config; - $this->util = $util; - $this->crypto = $crypto; } protected function configure() { diff --git a/core/Command/Encryption/SetDefaultModule.php b/core/Command/Encryption/SetDefaultModule.php index b50e004867f..f4106926778 100644 --- a/core/Command/Encryption/SetDefaultModule.php +++ b/core/Command/Encryption/SetDefaultModule.php @@ -31,16 +31,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class SetDefaultModule extends Command { - protected IManager $encryptionManager; - protected IConfig $config; - public function __construct( - IManager $encryptionManager, - IConfig $config + protected IManager $encryptionManager, + protected IConfig $config, ) { parent::__construct(); - $this->encryptionManager = $encryptionManager; - $this->config = $config; } protected function configure() { diff --git a/core/Command/Encryption/ShowKeyStorageRoot.php b/core/Command/Encryption/ShowKeyStorageRoot.php index 1c4f2b4cb4a..71b396540fd 100644 --- a/core/Command/Encryption/ShowKeyStorageRoot.php +++ b/core/Command/Encryption/ShowKeyStorageRoot.php @@ -29,11 +29,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ShowKeyStorageRoot extends Command { - protected Util $util; - - public function __construct(Util $util) { + public function __construct( + protected Util $util, + ) { parent::__construct(); - $this->util = $util; } protected function configure() { diff --git a/core/Command/Encryption/Status.php b/core/Command/Encryption/Status.php index 34ebabe1b73..691b399203d 100644 --- a/core/Command/Encryption/Status.php +++ b/core/Command/Encryption/Status.php @@ -27,11 +27,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Status extends Base { - protected IManager $encryptionManager; - - public function __construct(IManager $encryptionManager) { + public function __construct( + protected IManager $encryptionManager, + ) { parent::__construct(); - $this->encryptionManager = $encryptionManager; } protected function configure() { diff --git a/core/Command/FilesMetadata/Get.php b/core/Command/FilesMetadata/Get.php new file mode 100644 index 00000000000..d1def992c8a --- /dev/null +++ b/core/Command/FilesMetadata/Get.php @@ -0,0 +1,119 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Maxence Lange <maxence@artificial-owl.com> + * + * @author Maxence Lange <maxence@artificial-owl.com> + * + * @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/>. + * + */ + +namespace OC\Core\Command\FilesMetadata; + +use OC\User\NoUserException; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\IFilesMetadataManager; +use Symfony\Component\Console\Command\Command; +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 Get extends Command { + public function __construct( + private IRootFolder $rootFolder, + private IFilesMetadataManager $filesMetadataManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('metadata:get') + ->setDescription('get stored metadata about a file, by its id') + ->addArgument( + 'fileId', + InputArgument::REQUIRED, + 'id of the file document' + ) + ->addArgument( + 'userId', + InputArgument::OPTIONAL, + 'file owner' + ) + ->addOption( + 'as-array', + '', + InputOption::VALUE_NONE, + 'display metadata as a simple key=>value array' + ) + ->addOption( + 'refresh', + '', + InputOption::VALUE_NONE, + 'refresh metadata' + ) + ->addOption( + 'reset', + '', + InputOption::VALUE_NONE, + 'refresh metadata from scratch' + ); + } + + /** + * @throws NotPermittedException + * @throws FilesMetadataNotFoundException + * @throws NoUserException + * @throws NotFoundException + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $fileId = (int)$input->getArgument('fileId'); + + if ($input->getOption('reset')) { + $this->filesMetadataManager->deleteMetadata($fileId); + if (!$input->getOption('refresh')) { + return self::SUCCESS; + } + } + + if ($input->getOption('refresh')) { + $node = $this->rootFolder->getUserFolder($input->getArgument('userId'))->getFirstNodeById($fileId); + if (!$node) { + throw new NotFoundException(); + } + $metadata = $this->filesMetadataManager->refreshMetadata( + $node, + IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND + ); + } else { + $metadata = $this->filesMetadataManager->getMetadata($fileId); + } + + if ($input->getOption('as-array')) { + $output->writeln(json_encode($metadata->asArray(), JSON_PRETTY_PRINT)); + } else { + $output->writeln(json_encode($metadata, JSON_PRETTY_PRINT)); + } + + return self::SUCCESS; + } +} diff --git a/core/Command/Group/Add.php b/core/Command/Group/Add.php index d205cef0696..40502762e95 100644 --- a/core/Command/Group/Add.php +++ b/core/Command/Group/Add.php @@ -36,10 +36,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Add extends Base { - protected IGroupManager $groupManager; - - public function __construct(IGroupManager $groupManager) { - $this->groupManager = $groupManager; + public function __construct( + protected IGroupManager $groupManager, + ) { parent::__construct(); } diff --git a/core/Command/Group/AddUser.php b/core/Command/Group/AddUser.php index 6638bcd4c6d..a66d2898ef9 100644 --- a/core/Command/Group/AddUser.php +++ b/core/Command/Group/AddUser.php @@ -34,12 +34,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class AddUser 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(); } diff --git a/core/Command/Group/Delete.php b/core/Command/Group/Delete.php index fd1074d6f61..c7cbf0aa0f6 100644 --- a/core/Command/Group/Delete.php +++ b/core/Command/Group/Delete.php @@ -35,10 +35,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Delete extends Base { - protected IGroupManager $groupManager; - - public function __construct(IGroupManager $groupManager) { - $this->groupManager = $groupManager; + public function __construct( + protected IGroupManager $groupManager, + ) { parent::__construct(); } diff --git a/core/Command/Group/Info.php b/core/Command/Group/Info.php index dc475581ac5..1dab56e1a89 100644 --- a/core/Command/Group/Info.php +++ b/core/Command/Group/Info.php @@ -35,10 +35,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Info extends Base { - protected IGroupManager $groupManager; - - public function __construct(IGroupManager $groupManager) { - $this->groupManager = $groupManager; + public function __construct( + protected IGroupManager $groupManager, + ) { parent::__construct(); } diff --git a/core/Command/Group/ListCommand.php b/core/Command/Group/ListCommand.php index 5100a00c60a..22ce4cb0317 100644 --- a/core/Command/Group/ListCommand.php +++ b/core/Command/Group/ListCommand.php @@ -32,10 +32,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ListCommand extends Base { - protected IGroupManager $groupManager; - - public function __construct(IGroupManager $groupManager) { - $this->groupManager = $groupManager; + public function __construct( + protected IGroupManager $groupManager, + ) { parent::__construct(); } @@ -76,6 +75,17 @@ class ListCommand extends Base { } /** + * @param IGroup $group + * @return string[] + */ + public function usersForGroup(IGroup $group) { + $users = array_keys($group->getUsers()); + return array_map(function ($userId) { + return (string)$userId; + }, $users); + } + + /** * @param IGroup[] $groups * @return array */ @@ -88,12 +98,12 @@ class ListCommand extends Base { $values = array_map(function (IGroup $group) { return [ 'backends' => $group->getBackendNames(), - 'users' => array_keys($group->getUsers()), + 'users' => $this->usersForGroup($group), ]; }, $groups); } else { $values = array_map(function (IGroup $group) { - return array_keys($group->getUsers()); + return $this->usersForGroup($group); }, $groups); } return array_combine($keys, $values); diff --git a/core/Command/Group/RemoveUser.php b/core/Command/Group/RemoveUser.php index c7b3a2d84e7..6c7d4ce4d84 100644 --- a/core/Command/Group/RemoveUser.php +++ b/core/Command/Group/RemoveUser.php @@ -34,12 +34,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class RemoveUser 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(); } diff --git a/core/Command/Info/File.php b/core/Command/Info/File.php new file mode 100644 index 00000000000..4afda280370 --- /dev/null +++ b/core/Command/Info/File.php @@ -0,0 +1,155 @@ +<?php + +declare(strict_types=1); + +namespace OC\Core\Command\Info; + +use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\View; +use OCA\Files_External\Config\ExternalMountPoint; +use OCA\GroupFolders\Mount\GroupMountPoint; +use OCP\Files\Folder; +use OCP\Files\IHomeStorage; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\IL10N; +use OCP\L10N\IFactory; +use OCP\Util; +use Symfony\Component\Console\Command\Command; +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 File extends Command { + private IL10N $l10n; + private View $rootView; + + public function __construct( + IFactory $l10nFactory, + private FileUtils $fileUtils, + private \OC\Encryption\Util $encryptionUtil + ) { + $this->l10n = $l10nFactory->get("core"); + parent::__construct(); + $this->rootView = new View(); + } + + protected function configure(): void { + $this + ->setName('info:file') + ->setDescription('get information for a file') + ->addArgument('file', InputArgument::REQUIRED, "File id or path") + ->addOption('children', 'c', InputOption::VALUE_NONE, "List children of folders"); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $fileInput = $input->getArgument('file'); + $showChildren = $input->getOption('children'); + $node = $this->fileUtils->getNode($fileInput); + if (!$node) { + $output->writeln("<error>file $fileInput not found</error>"); + return 1; + } + + $output->writeln($node->getName()); + $output->writeln(" fileid: " . $node->getId()); + $output->writeln(" mimetype: " . $node->getMimetype()); + $output->writeln(" modified: " . (string)$this->l10n->l("datetime", $node->getMTime())); + $output->writeln(" " . ($node->isEncrypted() ? "encrypted" : "not encrypted")); + if ($node->isEncrypted()) { + $keyPath = $this->encryptionUtil->getFileKeyDir('', $node->getPath()); + if ($this->rootView->file_exists($keyPath)) { + $output->writeln(" encryption key at: " . $keyPath); + } else { + $output->writeln(" <error>encryption key not found</error> should be located at: " . $keyPath); + } + } + $output->writeln(" size: " . Util::humanFileSize($node->getSize())); + $output->writeln(" etag: " . $node->getEtag()); + if ($node instanceof Folder) { + $children = $node->getDirectoryListing(); + $childSize = array_sum(array_map(function (Node $node) { + return $node->getSize(); + }, $children)); + if ($childSize != $node->getSize()) { + $output->writeln(" <error>warning: folder has a size of " . Util::humanFileSize($node->getSize()) ." but it's children sum up to " . Util::humanFileSize($childSize) . "</error>."); + $output->writeln(" Run <info>occ files:scan --path " . $node->getPath() . "</info> to attempt to resolve this."); + } + if ($showChildren) { + $output->writeln(" children: " . count($children) . ":"); + foreach ($children as $child) { + $output->writeln(" - " . $child->getName()); + } + } else { + $output->writeln(" children: " . count($children) . " (use <info>--children</info> option to list)"); + } + } + $this->outputStorageDetails($node->getMountPoint(), $node, $output); + + $filesPerUser = $this->fileUtils->getFilesByUser($node); + $output->writeln(""); + $output->writeln("The following users have access to the file"); + $output->writeln(""); + foreach ($filesPerUser as $user => $files) { + $output->writeln("$user:"); + foreach ($files as $userFile) { + $output->writeln(" " . $userFile->getPath() . ": " . $this->fileUtils->formatPermissions($userFile->getType(), $userFile->getPermissions())); + $mount = $userFile->getMountPoint(); + $output->writeln(" " . $this->fileUtils->formatMountType($mount)); + } + } + + return 0; + } + + /** + * @psalm-suppress UndefinedClass + * @psalm-suppress UndefinedInterfaceMethod + */ + private function outputStorageDetails(IMountPoint $mountPoint, Node $node, OutputInterface $output): void { + $storage = $mountPoint->getStorage(); + if (!$storage) { + return; + } + if (!$storage->instanceOfStorage(IHomeStorage::class)) { + $output->writeln(" mounted at: " . $mountPoint->getMountPoint()); + } + if ($storage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $storage */ + $objectStoreId = $storage->getObjectStore()->getStorageId(); + $parts = explode(':', $objectStoreId); + /** @var string $bucket */ + $bucket = array_pop($parts); + $output->writeln(" bucket: " . $bucket); + if ($node instanceof \OC\Files\Node\File) { + $output->writeln(" object id: " . $storage->getURN($node->getId())); + try { + $fh = $node->fopen('r'); + if (!$fh) { + throw new NotFoundException(); + } + $stat = fstat($fh); + fclose($fh); + if ($stat['size'] !== $node->getSize()) { + $output->writeln(" <error>warning: object had a size of " . $stat['size'] . " but cache entry has a size of " . $node->getSize() . "</error>. This should have been automatically repaired"); + } + } catch (\Exception $e) { + $output->writeln(" <error>warning: object not found in bucket</error>"); + } + } + } else { + if (!$storage->file_exists($node->getInternalPath())) { + $output->writeln(" <error>warning: file not found in storage</error>"); + } + } + if ($mountPoint instanceof ExternalMountPoint) { + $storageConfig = $mountPoint->getStorageConfig(); + $output->writeln(" external storage id: " . $storageConfig->getId()); + $output->writeln(" external type: " . $storageConfig->getBackend()->getText()); + } elseif ($mountPoint instanceof GroupMountPoint) { + $output->writeln(" groupfolder id: " . $mountPoint->getFolderId()); + } + } +} diff --git a/core/Command/Info/FileUtils.php b/core/Command/Info/FileUtils.php new file mode 100644 index 00000000000..595a0216a5c --- /dev/null +++ b/core/Command/Info/FileUtils.php @@ -0,0 +1,237 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2023 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/>. + * + */ + +namespace OC\Core\Command\Info; + +use OCA\Circles\MountManager\CircleMount; +use OCA\Files_External\Config\ExternalMountPoint; +use OCA\Files_Sharing\SharedMount; +use OCA\GroupFolders\Mount\GroupMountPoint; +use OCP\Constants; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IHomeStorage; +use OCP\Files\IRootFolder; +use OCP\Files\Mount\IMountPoint; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Share\IShare; +use OCP\Util; +use Symfony\Component\Console\Output\OutputInterface; + +class FileUtils { + public function __construct( + private IRootFolder $rootFolder, + private IUserMountCache $userMountCache, + ) { + } + + /** + * @param FileInfo $file + * @return array<string, Node[]> + * @throws \OCP\Files\NotPermittedException + * @throws \OC\User\NoUserException + */ + public function getFilesByUser(FileInfo $file): array { + $id = $file->getId(); + if (!$id) { + return []; + } + + $mounts = $this->userMountCache->getMountsForFileId($id); + $result = []; + foreach ($mounts as $mount) { + if (isset($result[$mount->getUser()->getUID()])) { + continue; + } + + $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); + $result[$mount->getUser()->getUID()] = $userFolder->getById($id); + } + + return $result; + } + + /** + * Get file by either id of path + * + * @param string $fileInput + * @return Node|null + */ + public function getNode(string $fileInput): ?Node { + if (is_numeric($fileInput)) { + $mounts = $this->userMountCache->getMountsForFileId((int)$fileInput); + if (!$mounts) { + return null; + } + $mount = $mounts[0]; + $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); + return $userFolder->getFirstNodeById((int)$fileInput); + } else { + try { + return $this->rootFolder->get($fileInput); + } catch (NotFoundException $e) { + return null; + } + } + } + + public function formatPermissions(string $type, int $permissions): string { + if ($permissions == Constants::PERMISSION_ALL || ($type === 'file' && $permissions == (Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE))) { + return "full permissions"; + } + + $perms = []; + $allPerms = [Constants::PERMISSION_READ => "read", Constants::PERMISSION_UPDATE => "update", Constants::PERMISSION_CREATE => "create", Constants::PERMISSION_DELETE => "delete", Constants::PERMISSION_SHARE => "share"]; + foreach ($allPerms as $perm => $name) { + if (($permissions & $perm) === $perm) { + $perms[] = $name; + } + } + + return implode(", ", $perms); + } + + /** + * @psalm-suppress UndefinedClass + * @psalm-suppress UndefinedInterfaceMethod + */ + public function formatMountType(IMountPoint $mountPoint): string { + $storage = $mountPoint->getStorage(); + if ($storage && $storage->instanceOfStorage(IHomeStorage::class)) { + return "home storage"; + } elseif ($mountPoint instanceof SharedMount) { + $share = $mountPoint->getShare(); + $shares = $mountPoint->getGroupedShares(); + $sharedBy = array_map(function (IShare $share) { + $shareType = $this->formatShareType($share); + if ($shareType) { + return $share->getSharedBy() . " (via " . $shareType . " " . $share->getSharedWith() . ")"; + } else { + return $share->getSharedBy(); + } + }, $shares); + $description = "shared by " . implode(', ', $sharedBy); + if ($share->getSharedBy() !== $share->getShareOwner()) { + $description .= " owned by " . $share->getShareOwner(); + } + return $description; + } elseif ($mountPoint instanceof GroupMountPoint) { + return "groupfolder " . $mountPoint->getFolderId(); + } elseif ($mountPoint instanceof ExternalMountPoint) { + return "external storage " . $mountPoint->getStorageConfig()->getId(); + } elseif ($mountPoint instanceof CircleMount) { + return "circle"; + } + return get_class($mountPoint); + } + + public function formatShareType(IShare $share): ?string { + switch ($share->getShareType()) { + case IShare::TYPE_GROUP: + return "group"; + case IShare::TYPE_CIRCLE: + return "circle"; + case IShare::TYPE_DECK: + return "deck"; + case IShare::TYPE_ROOM: + return "room"; + case IShare::TYPE_USER: + return null; + default: + return "Unknown (" . $share->getShareType() . ")"; + } + } + + /** + * Print out the largest count($sizeLimits) files in the directory tree + * + * @param OutputInterface $output + * @param Folder $node + * @param string $prefix + * @param array $sizeLimits largest items that are still in the queue to be printed, ordered ascending + * @return int how many items we've printed + */ + public function outputLargeFilesTree( + OutputInterface $output, + Folder $node, + string $prefix, + array &$sizeLimits, + bool $all, + ): int { + /** + * Algorithm to print the N largest items in a folder without requiring to query or sort the entire three + * + * This is done by keeping a list ($sizeLimits) of size N that contain the largest items outside of this + * folders that are could be printed if there aren't enough items in this folder that are larger. + * + * We loop over the items in this folder by size descending until the size of the item falls before the smallest + * size in $sizeLimits (at that point there are enough items outside this folder to complete the N items). + * + * When encountering a folder, we create an updated $sizeLimits with the largest items in the current folder still + * remaining which we pass into the recursion. (We don't update the current $sizeLimits because that should only + * hold items *outside* of the current folder.) + * + * For every item printed we remove the first item of $sizeLimits are there is no longer room in the output to print + * items that small. + */ + + $count = 0; + $children = $node->getDirectoryListing(); + usort($children, function (Node $a, Node $b) { + return $b->getSize() <=> $a->getSize(); + }); + foreach ($children as $i => $child) { + if (!$all) { + if (count($sizeLimits) === 0 || $child->getSize() < $sizeLimits[0]) { + return $count; + } + array_shift($sizeLimits); + } + $count += 1; + + /** @var Node $child */ + $output->writeln("$prefix- " . $child->getName() . ": <info>" . Util::humanFileSize($child->getSize()) . "</info>"); + if ($child instanceof Folder) { + $recurseSizeLimits = $sizeLimits; + if (!$all) { + for ($j = 0; $j < count($recurseSizeLimits); $j++) { + if (isset($children[$i + $j + 1])) { + $nextChildSize = $children[$i + $j + 1]->getSize(); + if ($nextChildSize > $recurseSizeLimits[0]) { + array_shift($recurseSizeLimits); + $recurseSizeLimits[] = $nextChildSize; + } + } + } + sort($recurseSizeLimits); + } + $recurseCount = $this->outputLargeFilesTree($output, $child, $prefix . " ", $recurseSizeLimits, $all); + $sizeLimits = array_slice($sizeLimits, $recurseCount); + $count += $recurseCount; + } + } + return $count; + } +} diff --git a/core/Command/Info/Space.php b/core/Command/Info/Space.php new file mode 100644 index 00000000000..dfbfcf848c1 --- /dev/null +++ b/core/Command/Info/Space.php @@ -0,0 +1,66 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2023 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/>. + * + */ + +namespace OC\Core\Command\Info; + +use OCP\Files\Folder; +use OCP\Util; +use Symfony\Component\Console\Command\Command; +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 Space extends Command { + public function __construct( + private FileUtils $fileUtils, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('info:file:space') + ->setDescription('Summarize space usage of specified folder') + ->addArgument('file', InputArgument::REQUIRED, "File id or path") + ->addOption('count', 'c', InputOption::VALUE_REQUIRED, "Number of items to display", 25) + ->addOption('all', 'a', InputOption::VALUE_NONE, "Display all items"); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $fileInput = $input->getArgument('file'); + $count = (int)$input->getOption('count'); + $all = $input->getOption('all'); + $node = $this->fileUtils->getNode($fileInput); + if (!$node) { + $output->writeln("<error>file $fileInput not found</error>"); + return 1; + } + $output->writeln($node->getName() . ": <info>" . Util::humanFileSize($node->getSize()) . "</info>"); + if ($node instanceof Folder) { + $limits = $all ? [] : array_fill(0, $count - 1, 0); + $this->fileUtils->outputLargeFilesTree($output, $node, '', $limits, $all); + } + return 0; + } +} diff --git a/core/Command/Integrity/CheckApp.php b/core/Command/Integrity/CheckApp.php index ebd502c3d29..fe999fed9fd 100644 --- a/core/Command/Integrity/CheckApp.php +++ b/core/Command/Integrity/CheckApp.php @@ -40,11 +40,10 @@ use Symfony\Component\Console\Output\OutputInterface; * @package OC\Core\Command\Integrity */ class CheckApp extends Base { - private Checker $checker; - - public function __construct(Checker $checker) { + public function __construct( + private Checker $checker, + ) { parent::__construct(); - $this->checker = $checker; } /** diff --git a/core/Command/Integrity/CheckCore.php b/core/Command/Integrity/CheckCore.php index 9436786cad9..0bcb75fa6be 100644 --- a/core/Command/Integrity/CheckCore.php +++ b/core/Command/Integrity/CheckCore.php @@ -36,11 +36,10 @@ use Symfony\Component\Console\Output\OutputInterface; * @package OC\Core\Command\Integrity */ class CheckCore extends Base { - private Checker $checker; - - public function __construct(Checker $checker) { + public function __construct( + private Checker $checker, + ) { parent::__construct(); - $this->checker = $checker; } /** diff --git a/core/Command/Integrity/SignApp.php b/core/Command/Integrity/SignApp.php index 8492511d597..ebdc714c8c2 100644 --- a/core/Command/Integrity/SignApp.php +++ b/core/Command/Integrity/SignApp.php @@ -40,17 +40,12 @@ use Symfony\Component\Console\Output\OutputInterface; * @package OC\Core\Command\Integrity */ class SignApp extends Command { - private Checker $checker; - private FileAccessHelper $fileAccessHelper; - private IURLGenerator $urlGenerator; - - public function __construct(Checker $checker, - FileAccessHelper $fileAccessHelper, - IURLGenerator $urlGenerator) { + public function __construct( + private Checker $checker, + private FileAccessHelper $fileAccessHelper, + private IURLGenerator $urlGenerator, + ) { parent::__construct(null); - $this->checker = $checker; - $this->fileAccessHelper = $fileAccessHelper; - $this->urlGenerator = $urlGenerator; } protected function configure() { diff --git a/core/Command/Integrity/SignCore.php b/core/Command/Integrity/SignCore.php index 55d356fcd6b..48cfcadd10d 100644 --- a/core/Command/Integrity/SignCore.php +++ b/core/Command/Integrity/SignCore.php @@ -39,14 +39,11 @@ use Symfony\Component\Console\Output\OutputInterface; * @package OC\Core\Command\Integrity */ class SignCore extends Command { - private Checker $checker; - private FileAccessHelper $fileAccessHelper; - - public function __construct(Checker $checker, - FileAccessHelper $fileAccessHelper) { + public function __construct( + private Checker $checker, + private FileAccessHelper $fileAccessHelper, + ) { parent::__construct(null); - $this->checker = $checker; - $this->fileAccessHelper = $fileAccessHelper; } protected function configure() { diff --git a/core/Command/Log/File.php b/core/Command/Log/File.php index f2c77e20174..978115d5aeb 100644 --- a/core/Command/Log/File.php +++ b/core/Command/Log/File.php @@ -36,10 +36,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class File extends Command implements Completion\CompletionAwareInterface { - protected IConfig $config; - - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + protected IConfig $config, + ) { parent::__construct(); } @@ -115,14 +114,12 @@ class File extends Command implements Completion\CompletionAwareInterface { } /** - * @param mixed $rotateSize * @throws \InvalidArgumentException */ - protected function validateRotateSize(&$rotateSize) { + protected function validateRotateSize(false|int|float $rotateSize): void { if ($rotateSize === false) { throw new \InvalidArgumentException('Error parsing log rotation file size'); } - $rotateSize = (int) $rotateSize; if ($rotateSize < 0) { throw new \InvalidArgumentException('Log rotation file size must be non-negative'); } diff --git a/core/Command/Log/Manage.php b/core/Command/Log/Manage.php index 34ec5ea54a8..7c25fdf8a6b 100644 --- a/core/Command/Log/Manage.php +++ b/core/Command/Log/Manage.php @@ -39,10 +39,9 @@ class Manage extends Command implements CompletionAwareInterface { public const DEFAULT_LOG_LEVEL = 2; public const DEFAULT_TIMEZONE = 'UTC'; - protected IConfig $config; - - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + protected IConfig $config, + ) { parent::__construct(); } @@ -141,18 +140,18 @@ class Manage extends Command implements CompletionAwareInterface { protected function convertLevelString($level) { $level = strtolower($level); switch ($level) { - case 'debug': - return 0; - case 'info': - return 1; - case 'warning': - case 'warn': - return 2; - case 'error': - case 'err': - return 3; - case 'fatal': - return 4; + case 'debug': + return 0; + case 'info': + return 1; + case 'warning': + case 'warn': + return 2; + case 'error': + case 'err': + return 3; + case 'fatal': + return 4; } throw new \InvalidArgumentException('Invalid log level string'); } @@ -164,16 +163,16 @@ class Manage extends Command implements CompletionAwareInterface { */ protected function convertLevelNumber($levelNum) { switch ($levelNum) { - case 0: - return 'Debug'; - case 1: - return 'Info'; - case 2: - return 'Warning'; - case 3: - return 'Error'; - case 4: - return 'Fatal'; + case 0: + return 'Debug'; + case 1: + return 'Info'; + case 2: + return 'Warning'; + case 3: + return 'Error'; + case 4: + return 'Fatal'; } throw new \InvalidArgumentException('Invalid log level number'); } @@ -189,11 +188,7 @@ class Manage extends Command implements CompletionAwareInterface { } elseif ($optionName === 'level') { return ['debug', 'info', 'warning', 'error', 'fatal']; } elseif ($optionName === 'timezone') { - $identifier = \DateTimeZone::listIdentifiers(); - if ($identifier === false) { - return []; - } - return $identifier; + return \DateTimeZone::listIdentifiers(); } return []; } diff --git a/core/Command/Maintenance/DataFingerprint.php b/core/Command/Maintenance/DataFingerprint.php index a57dc307b18..3c15b95bc05 100644 --- a/core/Command/Maintenance/DataFingerprint.php +++ b/core/Command/Maintenance/DataFingerprint.php @@ -29,13 +29,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class DataFingerprint extends Command { - protected IConfig $config; - protected ITimeFactory $timeFactory; - - public function __construct(IConfig $config, - ITimeFactory $timeFactory) { - $this->config = $config; - $this->timeFactory = $timeFactory; + public function __construct( + protected IConfig $config, + protected ITimeFactory $timeFactory, + ) { parent::__construct(); } diff --git a/core/Command/Maintenance/Install.php b/core/Command/Maintenance/Install.php index c445f2c2f46..f5d28db23e3 100644 --- a/core/Command/Maintenance/Install.php +++ b/core/Command/Maintenance/Install.php @@ -1,4 +1,7 @@ <?php + +declare(strict_types=1); + /** * @copyright Copyright (c) 2016, ownCloud, Inc. * @@ -32,11 +35,10 @@ namespace OC\Core\Command\Maintenance; use bantu\IniGetWrapper\IniGetWrapper; use InvalidArgumentException; -use OC\Installer; +use OC\Console\TimestampFormatter; +use OC\Migration\ConsoleOutput; use OC\Setup; use OC\SystemConfig; -use OCP\Defaults; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -47,16 +49,14 @@ use Throwable; use function get_class; class Install extends Command { - private SystemConfig $config; - private IniGetWrapper $iniGetWrapper; - - public function __construct(SystemConfig $config, IniGetWrapper $iniGetWrapper) { + public function __construct( + private SystemConfig $config, + private IniGetWrapper $iniGetWrapper, + ) { parent::__construct(); - $this->config = $config; - $this->iniGetWrapper = $iniGetWrapper; } - protected function configure() { + protected function configure(): void { $this ->setName('maintenance:install') ->setDescription('install Nextcloud') @@ -64,28 +64,18 @@ class Install extends Command { ->addOption('database-name', null, InputOption::VALUE_REQUIRED, 'Name of the database') ->addOption('database-host', null, InputOption::VALUE_REQUIRED, 'Hostname of the database', 'localhost') ->addOption('database-port', null, InputOption::VALUE_REQUIRED, 'Port the database is listening on') - ->addOption('database-user', null, InputOption::VALUE_REQUIRED, 'User name to connect to the database') + ->addOption('database-user', null, InputOption::VALUE_REQUIRED, 'Login to connect to the database') ->addOption('database-pass', null, InputOption::VALUE_OPTIONAL, 'Password of the database user', null) ->addOption('database-table-space', null, InputOption::VALUE_OPTIONAL, 'Table space of the database (oci only)', null) - ->addOption('admin-user', null, InputOption::VALUE_REQUIRED, 'User name of the admin account', 'admin') + ->addOption('admin-user', null, InputOption::VALUE_REQUIRED, 'Login of the admin account', 'admin') ->addOption('admin-pass', null, InputOption::VALUE_REQUIRED, 'Password of the admin account') ->addOption('admin-email', null, InputOption::VALUE_OPTIONAL, 'E-Mail of the admin account') ->addOption('data-dir', null, InputOption::VALUE_REQUIRED, 'Path to data directory', \OC::$SERVERROOT."/data"); } protected function execute(InputInterface $input, OutputInterface $output): int { - // validate the environment - $server = \OC::$server; - $setupHelper = new Setup( - $this->config, - $this->iniGetWrapper, - $server->getL10N('lib'), - $server->query(Defaults::class), - $server->get(LoggerInterface::class), - $server->getSecureRandom(), - \OC::$server->query(Installer::class) - ); + $setupHelper = \OCP\Server::get(\OC\Setup::class); $sysInfo = $setupHelper->getSystemInfo(true); $errors = $sysInfo['errors']; if (count($errors) > 0) { @@ -101,12 +91,24 @@ class Install extends Command { // validate user input $options = $this->validateInput($input, $output, array_keys($sysInfo['databases'])); + if ($output->isVerbose()) { + // Prepend each line with a little timestamp + $timestampFormatter = new TimestampFormatter(null, $output->getFormatter()); + $output->setFormatter($timestampFormatter); + $migrationOutput = new ConsoleOutput($output); + } else { + $migrationOutput = null; + } + // perform installation - $errors = $setupHelper->install($options); + $errors = $setupHelper->install($options, $migrationOutput); if (count($errors) > 0) { $this->printErrors($output, $errors); return 1; } + if ($setupHelper->shouldRemoveCanInstallFile()) { + $output->writeln('<warn>Could not remove CAN_INSTALL from the config folder. Please remove this file manually.</warn>'); + } $output->writeln("Nextcloud was successfully installed"); return 0; } @@ -121,7 +123,7 @@ class Install extends Command { $db = strtolower($input->getOption('database')); if (!in_array($db, $supportedDatabases)) { - throw new InvalidArgumentException("Database <$db> is not supported."); + throw new InvalidArgumentException("Database <$db> is not supported. " . implode(", ", $supportedDatabases) . " are supported."); } $dbUser = $input->getOption('database-user'); @@ -148,7 +150,7 @@ class Install extends Command { if ($db !== 'sqlite') { if (is_null($dbUser)) { - throw new InvalidArgumentException("Database user not provided."); + throw new InvalidArgumentException("Database account not provided."); } if (is_null($dbName)) { throw new InvalidArgumentException("Database name not provided."); @@ -195,9 +197,9 @@ class Install extends Command { /** * @param OutputInterface $output - * @param $errors + * @param array<string|array> $errors */ - protected function printErrors(OutputInterface $output, $errors) { + protected function printErrors(OutputInterface $output, array $errors): void { foreach ($errors as $error) { if (is_array($error)) { $output->writeln('<error>' . $error['error'] . '</error>'); diff --git a/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php index 97432473722..873744e6f94 100644 --- a/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php +++ b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php @@ -30,17 +30,15 @@ namespace OC\Core\Command\Maintenance\Mimetype; class GenerateMimetypeFileBuilder { /** * Generate mime type list file - * @param $aliases + * + * @param array $aliases * @return string */ public function generateFile(array $aliases): string { // Remove comments - $keys = array_filter(array_keys($aliases), function ($k) { - return $k[0] === '_'; - }); - foreach ($keys as $key) { - unset($aliases[$key]); - } + $aliases = array_filter($aliases, static function ($key) { + return !($key === '' || $key[0] === '_'); + }, ARRAY_FILTER_USE_KEY); // Fetch all files $dir = new \DirectoryIterator(\OC::$SERVERROOT.'/core/img/filetypes'); diff --git a/core/Command/Maintenance/Mimetype/UpdateDB.php b/core/Command/Maintenance/Mimetype/UpdateDB.php index edc42c0fdcd..212b1994263 100644 --- a/core/Command/Maintenance/Mimetype/UpdateDB.php +++ b/core/Command/Maintenance/Mimetype/UpdateDB.php @@ -35,16 +35,11 @@ use Symfony\Component\Console\Output\OutputInterface; class UpdateDB extends Command { public const DEFAULT_MIMETYPE = 'application/octet-stream'; - protected IMimeTypeDetector $mimetypeDetector; - protected IMimeTypeLoader $mimetypeLoader; - public function __construct( - IMimeTypeDetector $mimetypeDetector, - IMimeTypeLoader $mimetypeLoader + protected IMimeTypeDetector $mimetypeDetector, + protected IMimeTypeLoader $mimetypeLoader, ) { parent::__construct(); - $this->mimetypeDetector = $mimetypeDetector; - $this->mimetypeLoader = $mimetypeLoader; } protected function configure() { diff --git a/core/Command/Maintenance/Mimetype/UpdateJS.php b/core/Command/Maintenance/Mimetype/UpdateJS.php index 6a5a3d0ac61..71145907b9e 100644 --- a/core/Command/Maintenance/Mimetype/UpdateJS.php +++ b/core/Command/Maintenance/Mimetype/UpdateJS.php @@ -31,13 +31,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class UpdateJS extends Command { - protected IMimeTypeDetector $mimetypeDetector; - public function __construct( - IMimeTypeDetector $mimetypeDetector + protected IMimeTypeDetector $mimetypeDetector, ) { parent::__construct(); - $this->mimetypeDetector = $mimetypeDetector; } protected function configure() { diff --git a/core/Command/Maintenance/Mode.php b/core/Command/Maintenance/Mode.php index c2af33aa4ed..685c2fa0837 100644 --- a/core/Command/Maintenance/Mode.php +++ b/core/Command/Maintenance/Mode.php @@ -33,10 +33,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Mode extends Command { - protected IConfig $config; - - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + protected IConfig $config, + ) { parent::__construct(); } diff --git a/core/Command/Maintenance/Repair.php b/core/Command/Maintenance/Repair.php index 2c1fda7c8e4..021e83e0833 100644 --- a/core/Command/Maintenance/Repair.php +++ b/core/Command/Maintenance/Repair.php @@ -29,29 +29,34 @@ namespace OC\Core\Command\Maintenance; use Exception; +use OC\Repair\Events\RepairAdvanceEvent; +use OC\Repair\Events\RepairErrorEvent; +use OC\Repair\Events\RepairFinishEvent; +use OC\Repair\Events\RepairInfoEvent; +use OC\Repair\Events\RepairStartEvent; +use OC\Repair\Events\RepairStepEvent; +use OC\Repair\Events\RepairWarningEvent; use OCP\App\IAppManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; class Repair extends Command { - protected \OC\Repair $repair; - protected IConfig $config; - private EventDispatcherInterface $dispatcher; private ProgressBar $progress; private OutputInterface $output; - private IAppManager $appManager; + protected bool $errored = false; - public function __construct(\OC\Repair $repair, IConfig $config, EventDispatcherInterface $dispatcher, IAppManager $appManager) { - $this->repair = $repair; - $this->config = $config; - $this->dispatcher = $dispatcher; - $this->appManager = $appManager; + public function __construct( + protected \OC\Repair $repair, + protected IConfig $config, + private IEventDispatcher $dispatcher, + private IAppManager $appManager, + ) { parent::__construct(); } @@ -97,52 +102,44 @@ class Repair extends Command { } } + + $maintenanceMode = $this->config->getSystemValueBool('maintenance'); $this->config->setSystemValue('maintenance', true); $this->progress = new ProgressBar($output); $this->output = $output; - $this->dispatcher->addListener('\OC\Repair::startProgress', [$this, 'handleRepairFeedBack']); - $this->dispatcher->addListener('\OC\Repair::advance', [$this, 'handleRepairFeedBack']); - $this->dispatcher->addListener('\OC\Repair::finishProgress', [$this, 'handleRepairFeedBack']); - $this->dispatcher->addListener('\OC\Repair::step', [$this, 'handleRepairFeedBack']); - $this->dispatcher->addListener('\OC\Repair::info', [$this, 'handleRepairFeedBack']); - $this->dispatcher->addListener('\OC\Repair::warning', [$this, 'handleRepairFeedBack']); - $this->dispatcher->addListener('\OC\Repair::error', [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairStartEvent::class, [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairAdvanceEvent::class, [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairFinishEvent::class, [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairStepEvent::class, [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairInfoEvent::class, [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairWarningEvent::class, [$this, 'handleRepairFeedBack']); + $this->dispatcher->addListener(RepairErrorEvent::class, [$this, 'handleRepairFeedBack']); $this->repair->run(); $this->config->setSystemValue('maintenance', $maintenanceMode); - return 0; + return $this->errored ? 1 : 0; } - public function handleRepairFeedBack($event) { - if (!$event instanceof GenericEvent) { - return; - } - switch ($event->getSubject()) { - case '\OC\Repair::startProgress': - $this->progress->start($event->getArgument(0)); - break; - case '\OC\Repair::advance': - $this->progress->advance($event->getArgument(0)); - break; - case '\OC\Repair::finishProgress': - $this->progress->finish(); - $this->output->writeln(''); - break; - case '\OC\Repair::step': - $this->output->writeln(' - ' . $event->getArgument(0)); - break; - case '\OC\Repair::info': - $this->output->writeln(' - ' . $event->getArgument(0)); - break; - case '\OC\Repair::warning': - $this->output->writeln(' - WARNING: ' . $event->getArgument(0)); - break; - case '\OC\Repair::error': - $this->output->writeln('<error> - ERROR: ' . $event->getArgument(0) . '</error>'); - break; + public function handleRepairFeedBack(Event $event): void { + if ($event instanceof RepairStartEvent) { + $this->progress->start($event->getMaxStep()); + } elseif ($event instanceof RepairAdvanceEvent) { + $this->progress->advance($event->getIncrement()); + } elseif ($event instanceof RepairFinishEvent) { + $this->progress->finish(); + $this->output->writeln(''); + } elseif ($event instanceof RepairStepEvent) { + $this->output->writeln('<info> - ' . $event->getStepName() . '</info>'); + } elseif ($event instanceof RepairInfoEvent) { + $this->output->writeln('<info> - ' . $event->getMessage() . '</info>'); + } elseif ($event instanceof RepairWarningEvent) { + $this->output->writeln('<comment> - WARNING: ' . $event->getMessage() . '</comment>'); + } elseif ($event instanceof RepairErrorEvent) { + $this->output->writeln('<error> - ERROR: ' . $event->getMessage() . '</error>'); + $this->errored = true; } } } diff --git a/core/Command/Maintenance/RepairShareOwnership.php b/core/Command/Maintenance/RepairShareOwnership.php new file mode 100644 index 00000000000..a89b9660a8c --- /dev/null +++ b/core/Command/Maintenance/RepairShareOwnership.php @@ -0,0 +1,191 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @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/>. + * + */ + +namespace OC\Core\Command\Maintenance; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +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\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class RepairShareOwnership extends Command { + public function __construct( + private IDBConnection $dbConnection, + private IUserManager $userManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('maintenance:repair-share-owner') + ->setDescription('repair invalid share-owner entries in the database') + ->addOption('no-confirm', 'y', InputOption::VALUE_NONE, "Don't ask for confirmation before repairing the shares") + ->addArgument('user', InputArgument::OPTIONAL, "User to fix incoming shares for, if omitted all users will be fixed"); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $noConfirm = $input->getOption('no-confirm'); + $userId = $input->getArgument('user'); + if ($userId) { + $user = $this->userManager->get($userId); + if (!$user) { + $output->writeln("<error>user $userId not found</error>"); + return 1; + } + $shares = $this->getWrongShareOwnershipForUser($user); + } else { + $shares = $this->getWrongShareOwnership(); + } + + if ($shares) { + $output->writeln(""); + $output->writeln("Found " . count($shares) . " shares with invalid share owner"); + foreach ($shares as $share) { + /** @var array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string} $share */ + $output->writeln(" - share {$share['shareId']} from \"{$share['initiator']}\" to \"{$share['receiver']}\" at \"{$share['fileTarget']}\", owned by \"{$share['owner']}\", that should be owned by \"{$share['mountOwner']}\""); + } + $output->writeln(""); + + if (!$noConfirm) { + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Repair these shares? [y/N]', false); + + if (!$helper->ask($input, $output, $question)) { + return 0; + } + } + $output->writeln("Repairing " . count($shares) . " shares"); + $this->repairShares($shares); + } else { + $output->writeln("Found no shares with invalid share owner"); + } + + return 0; + } + + /** + * @return array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] + * @throws \OCP\DB\Exception + */ + protected function getWrongShareOwnership(): array { + $qb = $this->dbConnection->getQueryBuilder(); + $brokenShares = $qb + ->select('s.id', 'm.user_id', 's.uid_owner', 's.uid_initiator', 's.share_with', 's.file_target') + ->from('share', 's') + ->join('s', 'filecache', 'f', $qb->expr()->eq('s.item_source', $qb->expr()->castColumn('f.fileid', IQueryBuilder::PARAM_STR))) + ->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id')) + ->where($qb->expr()->neq('m.user_id', 's.uid_owner')) + ->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), 'm.user_id', $qb->expr()->literal('/')), 'm.mount_point')) + ->executeQuery() + ->fetchAll(); + + $found = []; + + foreach ($brokenShares as $share) { + $found[] = [ + 'shareId' => (int) $share['id'], + 'fileTarget' => $share['file_target'], + 'initiator' => $share['uid_initiator'], + 'receiver' => $share['share_with'], + 'owner' => $share['uid_owner'], + 'mountOwner' => $share['user_id'], + ]; + } + + return $found; + } + + /** + * @param IUser $user + * @return array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] + * @throws \OCP\DB\Exception + */ + protected function getWrongShareOwnershipForUser(IUser $user): array { + $qb = $this->dbConnection->getQueryBuilder(); + $brokenShares = $qb + ->select('s.id', 'm.user_id', 's.uid_owner', 's.uid_initiator', 's.share_with', 's.file_target') + ->from('share', 's') + ->join('s', 'filecache', 'f', $qb->expr()->eq('s.item_source', $qb->expr()->castColumn('f.fileid', IQueryBuilder::PARAM_STR))) + ->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id')) + ->where($qb->expr()->neq('m.user_id', 's.uid_owner')) + ->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), 'm.user_id', $qb->expr()->literal('/')), 'm.mount_point')) + ->andWhere($qb->expr()->eq('s.share_with', $qb->createNamedParameter($user->getUID()))) + ->executeQuery() + ->fetchAll(); + + $found = []; + + foreach ($brokenShares as $share) { + $found[] = [ + 'shareId' => (int) $share['id'], + 'fileTarget' => $share['file_target'], + 'initiator' => $share['uid_initiator'], + 'receiver' => $share['share_with'], + 'owner' => $share['uid_owner'], + 'mountOwner' => $share['user_id'], + ]; + } + + return $found; + } + + /** + * @param array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] $shares + * @return void + */ + protected function repairShares(array $shares) { + $this->dbConnection->beginTransaction(); + + $update = $this->dbConnection->getQueryBuilder(); + $update->update('share') + ->set('uid_owner', $update->createParameter('share_owner')) + ->set('uid_initiator', $update->createParameter('share_initiator')) + ->where($update->expr()->eq('id', $update->createParameter('share_id'))); + + foreach ($shares as $share) { + /** @var array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string} $share */ + $update->setParameter('share_id', $share['shareId'], IQueryBuilder::PARAM_INT); + $update->setParameter('share_owner', $share['mountOwner']); + + // if the broken owner is also the initiator it's safe to update them both, otherwise we don't touch the initiator + if ($share['initiator'] === $share['owner']) { + $update->setParameter('share_initiator', $share['mountOwner']); + } else { + $update->setParameter('share_initiator', $share['initiator']); + } + $update->executeStatement(); + } + + $this->dbConnection->commit(); + } +} diff --git a/core/Command/Maintenance/UpdateHtaccess.php b/core/Command/Maintenance/UpdateHtaccess.php index 67c6db22b21..9243567afb4 100644 --- a/core/Command/Maintenance/UpdateHtaccess.php +++ b/core/Command/Maintenance/UpdateHtaccess.php @@ -39,7 +39,7 @@ class UpdateHtaccess extends Command { $output->writeln('.htaccess has been updated'); return 0; } else { - $output->writeln('<error>Error updating .htaccess file, not enough permissions or "overwrite.cli.url" set to an invalid URL?</error>'); + $output->writeln('<error>Error updating .htaccess file, not enough permissions, not enough free space or "overwrite.cli.url" set to an invalid URL?</error>'); return 1; } } diff --git a/core/Command/Maintenance/UpdateTheme.php b/core/Command/Maintenance/UpdateTheme.php index e469b218b3f..859aaa12d4f 100644 --- a/core/Command/Maintenance/UpdateTheme.php +++ b/core/Command/Maintenance/UpdateTheme.php @@ -33,15 +33,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class UpdateTheme extends UpdateJS { - protected IMimeTypeDetector $mimetypeDetector; - protected ICacheFactory $cacheFactory; - public function __construct( IMimeTypeDetector $mimetypeDetector, - ICacheFactory $cacheFactory + protected ICacheFactory $cacheFactory, ) { parent::__construct($mimetypeDetector); - $this->cacheFactory = $cacheFactory; } protected function configure() { diff --git a/core/Command/Preview/Generate.php b/core/Command/Preview/Generate.php new file mode 100644 index 00000000000..86528319199 --- /dev/null +++ b/core/Command/Preview/Generate.php @@ -0,0 +1,133 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2023 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/>. + * + */ + +namespace OC\Core\Command\Preview; + +use OCP\Files\Config\IUserMountCache; +use OCP\Files\File; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\IPreview; +use Symfony\Component\Console\Command\Command; +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 Generate extends Command { + public function __construct( + private IRootFolder $rootFolder, + private IUserMountCache $userMountCache, + private IPreview $previewManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('preview:generate') + ->setDescription('generate a preview for a file') + ->addArgument("file", InputArgument::REQUIRED, "path or fileid of the file to generate the preview for") + ->addOption("size", "s", InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, "size to generate the preview for in pixels, defaults to 64x64", ["64x64"]) + ->addOption("crop", "c", InputOption::VALUE_NONE, "crop the previews instead of maintaining aspect ratio") + ->addOption("mode", "m", InputOption::VALUE_REQUIRED, "mode for generating uncropped previews, 'cover' or 'fill'", IPreview::MODE_FILL); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $fileInput = $input->getArgument("file"); + $sizes = $input->getOption("size"); + $sizes = array_map(function (string $size) use ($output, &$error) { + if (str_contains($size, 'x')) { + $sizeParts = explode('x', $size, 2); + } else { + $sizeParts = [$size, $size]; + } + if (!is_numeric($sizeParts[0]) || !is_numeric($sizeParts[1])) { + $output->writeln("<error>Invalid size $size</error>"); + return null; + } + + return array_map("intval", $sizeParts); + }, $sizes); + if (in_array(null, $sizes)) { + return 1; + } + + $mode = $input->getOption("mode"); + if ($mode !== IPreview::MODE_FILL && $mode !== IPreview::MODE_COVER) { + $output->writeln("<error>Invalid mode $mode</error>"); + return 1; + } + $crop = $input->getOption("crop"); + $file = $this->getFile($fileInput); + if (!$file) { + $output->writeln("<error>File $fileInput not found</error>"); + return 1; + } + if (!$file instanceof File) { + $output->writeln("<error>Can't generate previews for folders</error>"); + return 1; + } + + if (!$this->previewManager->isAvailable($file)) { + $output->writeln("<error>No preview generator available for file of type" . $file->getMimetype() . "</error>"); + return 1; + } + + $specifications = array_map(function (array $sizes) use ($crop, $mode) { + return [ + 'width' => $sizes[0], + 'height' => $sizes[1], + 'crop' => $crop, + 'mode' => $mode, + ]; + }, $sizes); + + $this->previewManager->generatePreviews($file, $specifications); + if (count($specifications) > 1) { + $output->writeln("generated <info>" . count($specifications) . "</info> previews"); + } else { + $output->writeln("preview generated"); + } + return 0; + } + + private function getFile(string $fileInput): ?Node { + if (is_numeric($fileInput)) { + $mounts = $this->userMountCache->getMountsForFileId((int)$fileInput); + if (!$mounts) { + return null; + } + $mount = $mounts[0]; + $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); + return $userFolder->getFirstNodeById((int)$fileInput); + } else { + try { + return $this->rootFolder->get($fileInput); + } catch (NotFoundException $e) { + return null; + } + } + } +} diff --git a/core/Command/Preview/Repair.php b/core/Command/Preview/Repair.php index 650f80f99d0..fcdea9c20c8 100644 --- a/core/Command/Preview/Repair.php +++ b/core/Command/Preview/Repair.php @@ -42,21 +42,20 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; +use function pcntl_signal; + class Repair extends Command { - protected IConfig $config; - private IRootFolder $rootFolder; - private LoggerInterface $logger; private bool $stopSignalReceived = false; private int $memoryLimit; private int $memoryTreshold; - private ILockingProvider $lockingProvider; - - public function __construct(IConfig $config, IRootFolder $rootFolder, LoggerInterface $logger, IniGetWrapper $phpIni, ILockingProvider $lockingProvider) { - $this->config = $config; - $this->rootFolder = $rootFolder; - $this->logger = $logger; - $this->lockingProvider = $lockingProvider; + public function __construct( + protected IConfig $config, + private IRootFolder $rootFolder, + private LoggerInterface $logger, + IniGetWrapper $phpIni, + private ILockingProvider $lockingProvider, + ) { $this->memoryLimit = (int)$phpIni->getBytes('memory_limit'); $this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024; @@ -147,7 +146,7 @@ class Repair extends Command { $output->writeln("A total of $total preview files need to be migrated."); $output->writeln(""); - $output->writeln("The migration will always migrate all previews of a single file in a batch. After each batch the process can be canceled by pressing CTRL-C. This fill finish the current batch and then stop the migration. This migration can then just be started and it will continue."); + $output->writeln("The migration will always migrate all previews of a single file in a batch. After each batch the process can be canceled by pressing CTRL-C. This will finish the current batch and then stop the migration. This migration can then just be started and it will continue."); if ($input->getOption('batch')) { $output->writeln('Batch mode active: migration is started right away.'); diff --git a/core/Command/Preview/ResetRenderedTexts.php b/core/Command/Preview/ResetRenderedTexts.php index df623651f83..ec57f632ac9 100644 --- a/core/Command/Preview/ResetRenderedTexts.php +++ b/core/Command/Preview/ResetRenderedTexts.php @@ -39,24 +39,14 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ResetRenderedTexts extends Command { - protected IDBConnection $connection; - protected IUserManager $userManager; - protected IAvatarManager $avatarManager; - private Root $previewFolder; - private IMimeTypeLoader $mimeTypeLoader; - - public function __construct(IDBConnection $connection, - IUserManager $userManager, - IAvatarManager $avatarManager, - Root $previewFolder, - IMimeTypeLoader $mimeTypeLoader) { + public function __construct( + protected IDBConnection $connection, + protected IUserManager $userManager, + protected IAvatarManager $avatarManager, + private Root $previewFolder, + private IMimeTypeLoader $mimeTypeLoader, + ) { parent::__construct(); - - $this->connection = $connection; - $this->userManager = $userManager; - $this->avatarManager = $avatarManager; - $this->previewFolder = $previewFolder; - $this->mimeTypeLoader = $mimeTypeLoader; } protected function configure() { diff --git a/core/Command/Security/BruteforceAttempts.php b/core/Command/Security/BruteforceAttempts.php new file mode 100644 index 00000000000..16cd9712864 --- /dev/null +++ b/core/Command/Security/BruteforceAttempts.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @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/>. + * + */ +namespace OC\Core\Command\Security; + +use OC\Core\Command\Base; +use OCP\Security\Bruteforce\IThrottler; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class BruteforceAttempts extends Base { + public function __construct( + protected IThrottler $throttler, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('security:bruteforce:attempts') + ->setDescription('Show bruteforce attempts status for a given IP address') + ->addArgument( + 'ipaddress', + InputArgument::REQUIRED, + 'IP address for which the attempts status is to be shown', + ) + ->addArgument( + 'action', + InputArgument::OPTIONAL, + 'Only count attempts for the given action', + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $ip = $input->getArgument('ipaddress'); + + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $output->writeln('<error>"' . $ip . '" is not a valid IP address</error>'); + return 1; + } + + $data = [ + 'bypass-listed' => $this->throttler->isBypassListed($ip), + 'attempts' => $this->throttler->getAttempts( + $ip, + (string) $input->getArgument('action'), + ), + 'delay' => $this->throttler->getDelay( + $ip, + (string) $input->getArgument('action'), + ), + ]; + + $this->writeArrayInOutputFormat($input, $output, $data); + + return 0; + } +} diff --git a/core/Command/Security/ResetBruteforceAttempts.php b/core/Command/Security/BruteforceResetAttempts.php index 8def0873bdf..40d7c6848b2 100644 --- a/core/Command/Security/ResetBruteforceAttempts.php +++ b/core/Command/Security/BruteforceResetAttempts.php @@ -1,4 +1,6 @@ <?php + +declare(strict_types=1); /** * @copyright Copyright (c) 2020, Johannes Riedel (johannes@johannes-riedel.de) * @@ -24,23 +26,22 @@ namespace OC\Core\Command\Security; use OC\Core\Command\Base; -use OC\Security\Bruteforce\Throttler; +use OCP\Security\Bruteforce\IThrottler; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class ResetBruteforceAttempts extends Base { - protected Throttler $throttler; - - public function __construct(Throttler $throttler) { - $this->throttler = $throttler; +class BruteforceResetAttempts extends Base { + public function __construct( + protected IThrottler $throttler, + ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('security:bruteforce:reset') - ->setDescription('resets bruteforce attemps for given IP address') + ->setDescription('resets bruteforce attempts for given IP address') ->addArgument( 'ipaddress', InputArgument::REQUIRED, diff --git a/core/Command/Security/ImportCertificate.php b/core/Command/Security/ImportCertificate.php index 9db7889e307..a7e9bd94e4a 100644 --- a/core/Command/Security/ImportCertificate.php +++ b/core/Command/Security/ImportCertificate.php @@ -30,10 +30,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ImportCertificate extends Base { - protected ICertificateManager $certificateManager; - - public function __construct(ICertificateManager $certificateManager) { - $this->certificateManager = $certificateManager; + public function __construct( + protected ICertificateManager $certificateManager, + ) { parent::__construct(); } diff --git a/core/Command/Security/ListCertificates.php b/core/Command/Security/ListCertificates.php index 15dd1812077..97791956386 100644 --- a/core/Command/Security/ListCertificates.php +++ b/core/Command/Security/ListCertificates.php @@ -26,18 +26,20 @@ use OC\Core\Command\Base; use OCP\ICertificate; use OCP\ICertificateManager; use OCP\IL10N; +use OCP\L10N\IFactory as IL10NFactory; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ListCertificates extends Base { - protected ICertificateManager $certificateManager; protected IL10N $l; - public function __construct(ICertificateManager $certificateManager, IL10N $l) { - $this->certificateManager = $certificateManager; - $this->l = $l; + public function __construct( + protected ICertificateManager $certificateManager, + IL10NFactory $l10nFactory, + ) { parent::__construct(); + $this->l = $l10nFactory->get('core'); } protected function configure() { diff --git a/core/Command/Security/RemoveCertificate.php b/core/Command/Security/RemoveCertificate.php index 2f9c6ff978a..7dcd2d02604 100644 --- a/core/Command/Security/RemoveCertificate.php +++ b/core/Command/Security/RemoveCertificate.php @@ -30,10 +30,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class RemoveCertificate extends Base { - protected ICertificateManager $certificateManager; - - public function __construct(ICertificateManager $certificateManager) { - $this->certificateManager = $certificateManager; + public function __construct( + protected ICertificateManager $certificateManager, + ) { parent::__construct(); } diff --git a/core/Command/SetupChecks.php b/core/Command/SetupChecks.php new file mode 100644 index 00000000000..e6e54fbcf22 --- /dev/null +++ b/core/Command/SetupChecks.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Côme Chilliet <come.chilliet@nextcloud.com> + * + * @author Côme Chilliet <come.chilliet@nextcloud.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC\Core\Command; + +use OCP\SetupCheck\ISetupCheckManager; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SetupChecks extends Base { + public function __construct( + private ISetupCheckManager $setupCheckManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + + $this + ->setName('setupchecks') + ->setDescription('Run setup checks and output the results') + ; + } + + /** + * @TODO move this method to a common service used by notifications, activity and this command + * @throws \InvalidArgumentException if a parameter has no name or no type + */ + private function richToParsed(string $message, array $parameters): string { + $placeholders = []; + $replacements = []; + foreach ($parameters as $placeholder => $parameter) { + $placeholders[] = '{' . $placeholder . '}'; + foreach (['name','type'] as $requiredField) { + if (!isset($parameter[$requiredField]) || !is_string($parameter[$requiredField])) { + throw new \InvalidArgumentException("Invalid rich object, {$requiredField} field is missing"); + } + } + $replacements[] = match($parameter['type']) { + 'user' => '@' . $parameter['name'], + 'file' => $parameter['path'] ?? $parameter['name'], + default => $parameter['name'], + }; + } + return str_replace($placeholders, $replacements, $message); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $results = $this->setupCheckManager->runAll(); + switch ($input->getOption('output')) { + case self::OUTPUT_FORMAT_JSON: + case self::OUTPUT_FORMAT_JSON_PRETTY: + $this->writeArrayInOutputFormat($input, $output, $results); + break; + default: + foreach ($results as $category => $checks) { + $output->writeln("\t{$category}:"); + foreach ($checks as $check) { + $styleTag = match ($check->getSeverity()) { + 'success' => 'info', + 'error' => 'error', + 'warning' => 'comment', + default => null, + }; + $emoji = match ($check->getSeverity()) { + 'success' => '✓', + 'error' => '✗', + 'warning' => '⚠', + default => 'ℹ', + }; + $verbosity = ($check->getSeverity() === 'error' ? OutputInterface::VERBOSITY_QUIET : OutputInterface::VERBOSITY_NORMAL); + $description = $check->getDescription(); + $descriptionParameters = $check->getDescriptionParameters(); + if ($description !== null && $descriptionParameters !== null) { + $description = $this->richToParsed($description, $descriptionParameters); + } + $output->writeln( + "\t\t". + ($styleTag !== null ? "<{$styleTag}>" : ''). + "{$emoji} ". + ($check->getName() ?? $check::class). + ($description !== null ? ': '.$description : ''). + ($styleTag !== null ? "</{$styleTag}>" : ''), + $verbosity + ); + } + } + } + foreach ($results as $category => $checks) { + foreach ($checks as $check) { + if ($check->getSeverity() !== 'success') { + return self::FAILURE; + } + } + } + return self::SUCCESS; + } +} diff --git a/core/Command/Status.php b/core/Command/Status.php index 45ccb28f5c4..57b831c7eaa 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -29,17 +29,15 @@ use OCP\Defaults; use OCP\IConfig; use OCP\Util; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Status extends Base { - private IConfig $config; - private Defaults $themingDefaults; - - public function __construct(IConfig $config, Defaults $themingDefaults) { + public function __construct( + private IConfig $config, + private Defaults $themingDefaults, + ) { parent::__construct('status'); - - $this->config = $config; - $this->themingDefaults = $themingDefaults; } protected function configure() { @@ -47,22 +45,40 @@ class Status extends Base { $this ->setDescription('show some status information') - ; + ->addOption( + 'exit-code', + 'e', + InputOption::VALUE_NONE, + 'exit with 0 if running in normal mode, 1 when in maintenance mode, 2 when `./occ upgrade` is needed. Does not write any output to STDOUT.' + ); } protected function execute(InputInterface $input, OutputInterface $output): int { + $maintenanceMode = $this->config->getSystemValueBool('maintenance', false); + $needUpgrade = Util::needUpgrade(); $values = [ 'installed' => $this->config->getSystemValueBool('installed', false), 'version' => implode('.', Util::getVersion()), 'versionstring' => OC_Util::getVersionString(), 'edition' => '', - 'maintenance' => $this->config->getSystemValueBool('maintenance', false), - 'needsDbUpgrade' => Util::needUpgrade(), + 'maintenance' => $maintenanceMode, + 'needsDbUpgrade' => $needUpgrade, 'productname' => $this->themingDefaults->getProductName(), 'extendedSupport' => Util::hasExtendedSupport() ]; - $this->writeArrayInOutputFormat($input, $output, $values); + if ($input->getOption('verbose') || !$input->getOption('exit-code')) { + $this->writeArrayInOutputFormat($input, $output, $values); + } + + if ($input->getOption('exit-code')) { + if ($maintenanceMode === true) { + return 1; + } + if ($needUpgrade === true) { + return 2; + } + } return 0; } } diff --git a/core/Command/SystemTag/Add.php b/core/Command/SystemTag/Add.php index f4fb80eb70a..067cd00118a 100644 --- a/core/Command/SystemTag/Add.php +++ b/core/Command/SystemTag/Add.php @@ -31,10 +31,9 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Add extends Base { - protected ISystemTagManager $systemTagManager; - - public function __construct(ISystemTagManager $systemTagManager) { - $this->systemTagManager = $systemTagManager; + public function __construct( + protected ISystemTagManager $systemTagManager, + ) { parent::__construct(); } diff --git a/core/Command/SystemTag/Delete.php b/core/Command/SystemTag/Delete.php index 4c1145ae1b4..ed893ae037c 100644 --- a/core/Command/SystemTag/Delete.php +++ b/core/Command/SystemTag/Delete.php @@ -30,10 +30,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Delete extends Base { - protected ISystemTagManager $systemTagManager; - - public function __construct(ISystemTagManager $systemTagManager) { - $this->systemTagManager = $systemTagManager; + public function __construct( + protected ISystemTagManager $systemTagManager, + ) { parent::__construct(); } diff --git a/core/Command/SystemTag/Edit.php b/core/Command/SystemTag/Edit.php index 7ed933c3b35..111dc500e79 100644 --- a/core/Command/SystemTag/Edit.php +++ b/core/Command/SystemTag/Edit.php @@ -31,10 +31,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Edit extends Base { - protected ISystemTagManager $systemTagManager; - - public function __construct(ISystemTagManager $systemTagManager) { - $this->systemTagManager = $systemTagManager; + public function __construct( + protected ISystemTagManager $systemTagManager, + ) { parent::__construct(); } diff --git a/core/Command/SystemTag/ListCommand.php b/core/Command/SystemTag/ListCommand.php index 7993eb87891..c0f4eba241c 100644 --- a/core/Command/SystemTag/ListCommand.php +++ b/core/Command/SystemTag/ListCommand.php @@ -30,10 +30,9 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class ListCommand extends Base { - protected ISystemTagManager $systemTagManager; - - public function __construct(ISystemTagManager $systemTagManager) { - $this->systemTagManager = $systemTagManager; + public function __construct( + protected ISystemTagManager $systemTagManager, + ) { parent::__construct(); } diff --git a/core/Command/TwoFactorAuth/Base.php b/core/Command/TwoFactorAuth/Base.php index 27bd381d951..a36cb2af374 100644 --- a/core/Command/TwoFactorAuth/Base.php +++ b/core/Command/TwoFactorAuth/Base.php @@ -30,7 +30,12 @@ use OCP\IUserManager; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; class Base extends \OC\Core\Command\Base { - protected IUserManager $userManager; + public function __construct( + ?string $name, + protected IUserManager $userManager, + ) { + parent::__construct($name); + } /** * Return possible values for the named option diff --git a/core/Command/TwoFactorAuth/Cleanup.php b/core/Command/TwoFactorAuth/Cleanup.php index 7d3fc3c33f7..1b2c6e22632 100644 --- a/core/Command/TwoFactorAuth/Cleanup.php +++ b/core/Command/TwoFactorAuth/Cleanup.php @@ -27,17 +27,20 @@ declare(strict_types=1); namespace OC\Core\Command\TwoFactorAuth; use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\IUserManager; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Cleanup extends Base { - private IRegistry $registry; - - public function __construct(IRegistry $registry) { - parent::__construct(); - - $this->registry = $registry; + public function __construct( + private IRegistry $registry, + IUserManager $userManager, + ) { + parent::__construct( + null, + $userManager, + ); } protected function configure() { diff --git a/core/Command/TwoFactorAuth/Disable.php b/core/Command/TwoFactorAuth/Disable.php index 54e4b138a0a..a593993128f 100644 --- a/core/Command/TwoFactorAuth/Disable.php +++ b/core/Command/TwoFactorAuth/Disable.php @@ -29,12 +29,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Disable extends Base { - private ProviderManager $manager; - - public function __construct(ProviderManager $manager, IUserManager $userManager) { - parent::__construct('twofactorauth:disable'); - $this->manager = $manager; - $this->userManager = $userManager; + public function __construct( + private ProviderManager $manager, + IUserManager $userManager, + ) { + parent::__construct( + 'twofactorauth:disable', + $userManager, + ); } protected function configure() { diff --git a/core/Command/TwoFactorAuth/Enable.php b/core/Command/TwoFactorAuth/Enable.php index 67c1778399d..b0d80c43a61 100644 --- a/core/Command/TwoFactorAuth/Enable.php +++ b/core/Command/TwoFactorAuth/Enable.php @@ -29,12 +29,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class Enable extends Base { - private ProviderManager $manager; - - public function __construct(ProviderManager $manager, IUserManager $userManager) { - parent::__construct('twofactorauth:enable'); - $this->manager = $manager; - $this->userManager = $userManager; + public function __construct( + private ProviderManager $manager, + IUserManager $userManager, + ) { + parent::__construct( + 'twofactorauth:enable', + $userManager, + ); } protected function configure() { diff --git a/core/Command/TwoFactorAuth/Enforce.php b/core/Command/TwoFactorAuth/Enforce.php index d8fa41e2e95..57b07308afe 100644 --- a/core/Command/TwoFactorAuth/Enforce.php +++ b/core/Command/TwoFactorAuth/Enforce.php @@ -26,21 +26,19 @@ declare(strict_types=1); */ namespace OC\Core\Command\TwoFactorAuth; -use function implode; use OC\Authentication\TwoFactorAuth\EnforcementState; use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function implode; class Enforce extends Command { - private MandatoryTwoFactor $mandatoryTwoFactor; - - public function __construct(MandatoryTwoFactor $mandatoryTwoFactor) { + public function __construct( + private MandatoryTwoFactor $mandatoryTwoFactor, + ) { parent::__construct(); - - $this->mandatoryTwoFactor = $mandatoryTwoFactor; } protected function configure() { diff --git a/core/Command/TwoFactorAuth/State.php b/core/Command/TwoFactorAuth/State.php index 4694c76b408..5663056b50a 100644 --- a/core/Command/TwoFactorAuth/State.php +++ b/core/Command/TwoFactorAuth/State.php @@ -33,13 +33,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class State extends Base { - private IRegistry $registry; - - public function __construct(IRegistry $registry, IUserManager $userManager) { - parent::__construct('twofactorauth:state'); - - $this->registry = $registry; - $this->userManager = $userManager; + public function __construct( + private IRegistry $registry, + IUserManager $userManager, + ) { + parent::__construct( + 'twofactorauth:state', + $userManager, + ); } protected function configure() { @@ -91,7 +92,7 @@ class State extends Base { } private function printProviders(string $title, array $providers, - OutputInterface $output) { + OutputInterface $output) { if (empty($providers)) { // Ignore and don't print anything return; diff --git a/core/Command/Upgrade.php b/core/Command/Upgrade.php index acf0b503d19..45427f6552f 100644 --- a/core/Command/Upgrade.php +++ b/core/Command/Upgrade.php @@ -34,16 +34,23 @@ namespace OC\Core\Command; use OC\Console\TimestampFormatter; -use OC\Installer; +use OC\DB\MigratorExecuteSqlEvent; +use OC\Repair\Events\RepairAdvanceEvent; +use OC\Repair\Events\RepairErrorEvent; +use OC\Repair\Events\RepairFinishEvent; +use OC\Repair\Events\RepairInfoEvent; +use OC\Repair\Events\RepairStartEvent; +use OC\Repair\Events\RepairStepEvent; +use OC\Repair\Events\RepairWarningEvent; use OC\Updater; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\Util; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\EventDispatcher\GenericEvent; class Upgrade extends Command { public const ERROR_SUCCESS = 0; @@ -53,15 +60,10 @@ class Upgrade extends Command { public const ERROR_INVALID_ARGUMENTS = 4; public const ERROR_FAILURE = 5; - private IConfig $config; - private LoggerInterface $logger; - private Installer $installer; - - public function __construct(IConfig $config, LoggerInterface $logger, Installer $installer) { + public function __construct( + private IConfig $config + ) { parent::__construct(); - $this->config = $config; - $this->logger = $logger; - $this->installer = $installer; } protected function configure() { @@ -85,91 +87,73 @@ class Upgrade extends Command { } $self = $this; - $updater = new Updater( - $this->config, - \OC::$server->getIntegrityCodeChecker(), - $this->logger, - $this->installer - ); + $updater = \OCP\Server::get(Updater::class); + $incompatibleOverwrites = $this->config->getSystemValue('app_install_overwrite', []); - $dispatcher = \OC::$server->getEventDispatcher(); + /** @var IEventDispatcher $dispatcher */ + $dispatcher = \OC::$server->get(IEventDispatcher::class); $progress = new ProgressBar($output); $progress->setFormat(" %message%\n %current%/%max% [%bar%] %percent:3s%%"); - $listener = function ($event) use ($progress, $output) { - if ($event instanceof GenericEvent) { - $message = $event->getSubject(); - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { - $output->writeln(' Checking table ' . $message); - } else { - if (strlen($message) > 60) { - $message = substr($message, 0, 57) . '...'; - } - $progress->setMessage($message); - if ($event[0] === 1) { - $output->writeln(''); - $progress->start($event[1]); - } - $progress->setProgress($event[0]); - if ($event[0] === $event[1]) { - $progress->setMessage('Done'); - $progress->finish(); - $output->writeln(''); - } + $listener = function (MigratorExecuteSqlEvent $event) use ($progress, $output): void { + $message = $event->getSql(); + if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + $output->writeln(' Executing SQL ' . $message); + } else { + if (strlen($message) > 60) { + $message = substr($message, 0, 57) . '...'; } - } - }; - $repairListener = function ($event) use ($progress, $output) { - if (!$event instanceof GenericEvent) { - return; - } - switch ($event->getSubject()) { - case '\OC\Repair::startProgress': - $progress->setMessage('Starting ...'); - $output->writeln($event->getArgument(1)); + $progress->setMessage($message); + if ($event->getCurrentStep() === 1) { $output->writeln(''); - $progress->start($event->getArgument(0)); - break; - case '\OC\Repair::advance': - $desc = $event->getArgument(1); - if (!empty($desc)) { - $progress->setMessage($desc); - } - $progress->advance($event->getArgument(0)); - - break; - case '\OC\Repair::finishProgress': + $progress->start($event->getMaxStep()); + } + $progress->setProgress($event->getCurrentStep()); + if ($event->getCurrentStep() === $event->getMaxStep()) { $progress->setMessage('Done'); $progress->finish(); $output->writeln(''); - break; - case '\OC\Repair::step': - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { - $output->writeln('<info>Repair step: ' . $event->getArgument(0) . '</info>'); - } - break; - case '\OC\Repair::info': - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { - $output->writeln('<info>Repair info: ' . $event->getArgument(0) . '</info>'); - } - break; - case '\OC\Repair::warning': - $output->writeln('<error>Repair warning: ' . $event->getArgument(0) . '</error>'); - break; - case '\OC\Repair::error': - $output->writeln('<error>Repair error: ' . $event->getArgument(0) . '</error>'); - break; + } + } + }; + $repairListener = function (Event $event) use ($progress, $output): void { + if ($event instanceof RepairStartEvent) { + $progress->setMessage('Starting ...'); + $output->writeln($event->getCurrentStepName()); + $output->writeln(''); + $progress->start($event->getMaxStep()); + } elseif ($event instanceof RepairAdvanceEvent) { + $desc = $event->getDescription(); + if (!empty($desc)) { + $progress->setMessage($desc); + } + $progress->advance($event->getIncrement()); + } elseif ($event instanceof RepairFinishEvent) { + $progress->setMessage('Done'); + $progress->finish(); + $output->writeln(''); + } elseif ($event instanceof RepairStepEvent) { + if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + $output->writeln('<info>Repair step: ' . $event->getStepName() . '</info>'); + } + } elseif ($event instanceof RepairInfoEvent) { + if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + $output->writeln('<info>Repair info: ' . $event->getMessage() . '</info>'); + } + } elseif ($event instanceof RepairWarningEvent) { + $output->writeln('<error>Repair warning: ' . $event->getMessage() . '</error>'); + } elseif ($event instanceof RepairErrorEvent) { + $output->writeln('<error>Repair error: ' . $event->getMessage() . '</error>'); } }; - $dispatcher->addListener('\OC\DB\Migrator::executeSql', $listener); - $dispatcher->addListener('\OC\DB\Migrator::checkTable', $listener); - $dispatcher->addListener('\OC\Repair::startProgress', $repairListener); - $dispatcher->addListener('\OC\Repair::advance', $repairListener); - $dispatcher->addListener('\OC\Repair::finishProgress', $repairListener); - $dispatcher->addListener('\OC\Repair::step', $repairListener); - $dispatcher->addListener('\OC\Repair::info', $repairListener); - $dispatcher->addListener('\OC\Repair::warning', $repairListener); - $dispatcher->addListener('\OC\Repair::error', $repairListener); + $dispatcher->addListener(MigratorExecuteSqlEvent::class, $listener); + $dispatcher->addListener(RepairStartEvent::class, $repairListener); + $dispatcher->addListener(RepairAdvanceEvent::class, $repairListener); + $dispatcher->addListener(RepairFinishEvent::class, $repairListener); + $dispatcher->addListener(RepairStepEvent::class, $repairListener); + $dispatcher->addListener(RepairInfoEvent::class, $repairListener); + $dispatcher->addListener(RepairWarningEvent::class, $repairListener); + $dispatcher->addListener(RepairErrorEvent::class, $repairListener); $updater->listen('\OC\Updater', 'maintenanceEnabled', function () use ($output) { @@ -196,8 +180,10 @@ class Upgrade extends Command { $updater->listen('\OC\Updater', 'dbUpgrade', function () use ($output) { $output->writeln('<info>Updated database</info>'); }); - $updater->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($output) { - $output->writeln('<comment>Disabled incompatible app: ' . $app . '</comment>'); + $updater->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($output, &$incompatibleOverwrites) { + if (!in_array($app, $incompatibleOverwrites)) { + $output->writeln('<comment>Disabled incompatible app: ' . $app . '</comment>'); + } }); $updater->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($output) { $output->writeln('<info>Update app ' . $app . ' from App Store</info>'); diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php index 24d11fbee6e..3411946fdeb 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -2,6 +2,7 @@ /** * @copyright Copyright (c) 2016, ownCloud, Inc. * + * @author Anupam Kumar <kyteinsky@gmail.com> * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author Joas Schilling <coding@schilljs.com> @@ -26,10 +27,16 @@ 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,23 +46,26 @@ 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', @@ -64,32 +74,52 @@ class Add extends Command { 'read password from environment variable 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'); + if (!$password) { $output->writeln('<error>--password-from-env given, but 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'); @@ -107,25 +137,24 @@ class Add extends Command { 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 +182,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..e055d210447 100644 --- a/core/Command/User/AddAppPassword.php +++ b/core/Command/User/AuthTokens/Add.php @@ -24,7 +24,7 @@ declare(strict_types=1); * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -namespace OC\Core\Command\User; +namespace OC\Core\Command\User\AuthTokens; use OC\Authentication\Events\AppPasswordCreatedEvent; use OC\Authentication\Token\IProvider; @@ -40,31 +40,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,7 +75,7 @@ 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; } @@ -95,7 +89,7 @@ class AddAppPassword extends Command { /** @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); diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php new file mode 100644 index 00000000000..56bfcf787f8 --- /dev/null +++ b/core/Command/User/AuthTokens/Delete.php @@ -0,0 +1,120 @@ +<?php +/** + * @copyright Copyright (c) 2023 Lucas Azevedo <lhs_azevedo@hotmail.com> + * + * @author Lucas Azevedo <lhs_azevedo@hotmail.com> + * + * @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/>. + * + */ +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..0bcd2e55225 --- /dev/null +++ b/core/Command/User/AuthTokens/ListCommand.php @@ -0,0 +1,100 @@ +<?php +/** + * @copyright Copyright (c) 2023 Lucas Azevedo <lhs_azevedo@hotmail.com> + * + * @author Lucas Azevedo <lhs_azevedo@hotmail.com> + * + * @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/>. + * + */ +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/Delete.php b/core/Command/User/Delete.php index 9624f04fa18..94390a3f070 100644 --- a/core/Command/User/Delete.php +++ b/core/Command/User/Delete.php @@ -33,14 +33,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..3fdf6220f45 100644 --- a/core/Command/User/Disable.php +++ b/core/Command/User/Disable.php @@ -32,10 +32,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..2055bd30cec 100644 --- a/core/Command/User/Enable.php +++ b/core/Command/User/Enable.php @@ -32,10 +32,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..a8fb62099e2 100644 --- a/core/Command/User/Info.php +++ b/core/Command/User/Info.php @@ -35,12 +35,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(); } diff --git a/core/Command/User/Keys/Verify.php b/core/Command/User/Keys/Verify.php new file mode 100644 index 00000000000..c4264457572 --- /dev/null +++ b/core/Command/User/Keys/Verify.php @@ -0,0 +1,100 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024, Marcel Müller <marcel.mueller@nextcloud.com> + * + * @author Marcel Müller <marcel.mueller@nextcloud.com> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +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..d78d8661ecf 100644 --- a/core/Command/User/LastSeen.php +++ b/core/Command/User/LastSeen.php @@ -31,44 +31,70 @@ 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')); + } + + 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) { + $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')); + } + }); return 0; } diff --git a/core/Command/User/ListCommand.php b/core/Command/User/ListCommand.php index c254a8a11cf..f25d6c2dae9 100644 --- a/core/Command/User/ListCommand.php +++ b/core/Command/User/ListCommand.php @@ -33,13 +33,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(); } @@ -74,7 +71,7 @@ class ListCommand extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $users = $this->userManager->search('', (int) $input->getOption('limit'), (int) $input->getOption('offset')); + $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; diff --git a/core/Command/User/Report.php b/core/Command/User/Report.php index e080a617258..b54ec2b0308 100644 --- a/core/Command/User/Report.php +++ b/core/Command/User/Report.php @@ -41,13 +41,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 +63,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..a94be81fed8 100644 --- a/core/Command/User/ResetPassword.php +++ b/core/Command/User/ResetPassword.php @@ -41,13 +41,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,7 +55,7 @@ class ResetPassword extends Base { ->addArgument( 'user', InputArgument::REQUIRED, - 'Username to reset password' + 'Login to reset password' ) ->addOption( 'password-from-env', diff --git a/core/Command/User/Setting.php b/core/Command/User/Setting.php index 6e7c15375d1..9134d174418 100644 --- a/core/Command/User/Setting.php +++ b/core/Command/User/Setting.php @@ -36,13 +36,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 +51,7 @@ class Setting extends Base { ->addArgument( 'uid', InputArgument::REQUIRED, - 'User ID used to login' + 'Account ID used to login' ) ->addArgument( 'app', @@ -113,9 +111,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')) { diff --git a/core/Command/User/SyncAccountDataCommand.php b/core/Command/User/SyncAccountDataCommand.php new file mode 100644 index 00000000000..6a4a600ea03 --- /dev/null +++ b/core/Command/User/SyncAccountDataCommand.php @@ -0,0 +1,105 @@ +<?php +/** + * @copyright Copyright (c) 2023 Julius Härrtl <jus@bitgrid.net> + * + * @author Julius Härrtl <jus@bitgrid.net> + * + * @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/>. + * + */ +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 { + protected IUserManager $userManager; + protected IAccountManager $accountManager; + + public function __construct( + IUserManager $userManager, + IAccountManager $accountManager + ) { + $this->userManager = $userManager; + $this->accountManager = $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'); + } + } +} |