diff options
Diffstat (limited to 'core/Command')
140 files changed, 3993 insertions, 3084 deletions
diff --git a/core/Command/App/Disable.php b/core/Command/App/Disable.php index c5abc6c95cf..121ad3f010c 100644 --- a/core/Command/App/Disable.php +++ b/core/Command/App/Disable.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\App; @@ -63,7 +45,7 @@ class Disable extends Command implements CompletionAwareInterface { } private function disableApp(string $appId, OutputInterface $output): void { - if ($this->appManager->isInstalled($appId) === false) { + if ($this->appManager->isEnabledForAnyone($appId) === false) { $output->writeln('No such app enabled: ' . $appId); return; } @@ -83,7 +65,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 +74,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 c3ab8be38ce..3936acfbf6e 100644 --- a/core/Command/App/Enable.php +++ b/core/Command/App/Enable.php @@ -1,27 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Sander Ruitenbeek <s.ruitenbeek@getgoing.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\App; @@ -44,6 +27,7 @@ class Enable extends Command implements CompletionAwareInterface { public function __construct( protected IAppManager $appManager, protected IGroupManager $groupManager, + private Installer $installer, ) { parent::__construct(); } @@ -74,7 +58,7 @@ class Enable extends Command implements CompletionAwareInterface { protected function execute(InputInterface $input, OutputInterface $output): int { $appIds = $input->getArgument('app-id'); $groups = $this->resolveGroupIds($input->getOption('groups')); - $forceEnable = (bool) $input->getOption('force'); + $forceEnable = (bool)$input->getOption('force'); foreach ($appIds as $appId) { $this->enableApp($appId, $groups, $forceEnable, $output); @@ -94,20 +78,17 @@ class Enable extends Command implements CompletionAwareInterface { return $group->getDisplayName(); }, $groupIds); - if ($this->appManager->isInstalled($appId) && $groupIds === []) { + if ($this->appManager->isEnabledForUser($appId) && $groupIds === []) { $output->writeln($appId . ' already enabled'); return; } try { - /** @var Installer $installer */ - $installer = \OC::$server->query(Installer::class); - - if (false === $installer->isDownloaded($appId)) { - $installer->downloadApp($appId); + if ($this->installer->isDownloaded($appId) === false) { + $this->installer->downloadApp($appId); } - $installer->installApp($appId, $forceEnable); + $this->installer->installApp($appId, $forceEnable); $appVersion = $this->appManager->getAppVersion($appId); if ($groupIds === []) { @@ -146,7 +127,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(); @@ -160,9 +141,9 @@ 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(); + $allApps = $this->appManager->getAllAppsInAppsFolders(); return array_diff($allApps, \OC_App::getEnabledApps(true, true)); } return []; diff --git a/core/Command/App/GetPath.php b/core/Command/App/GetPath.php index 2ec72385191..3ba4ed7781b 100644 --- a/core/Command/App/GetPath.php +++ b/core/Command/App/GetPath.php @@ -1,35 +1,29 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\App; use OC\Core\Command\Base; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class GetPath extends Base { - protected function configure() { + public function __construct( + protected IAppManager $appManager, + ) { + parent::__construct(); + } + + protected function configure(): void { parent::configure(); $this @@ -46,20 +40,20 @@ class GetPath extends Base { /** * Executes the current command. * - * @param InputInterface $input An InputInterface instance + * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance * @return int 0 if everything went fine, or an error code */ protected function execute(InputInterface $input, OutputInterface $output): int { $appName = $input->getArgument('app'); - $path = \OC_App::getAppPath($appName); - if ($path !== false) { - $output->writeln($path); - return 0; + try { + $path = $this->appManager->getAppPath($appName); + } catch (AppPathNotFoundException) { + // App not found, exit with non-zero + return self::FAILURE; } - - // App not found, exit with non-zero - return 1; + $output->writeln($path); + return self::SUCCESS; } /** @@ -67,9 +61,9 @@ 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(); + return $this->appManager->getAllAppsInAppsFolders(); } return []; } diff --git a/core/Command/App/Install.php b/core/Command/App/Install.php index d87439b3664..c8a396c8e36 100644 --- a/core/Command/App/Install.php +++ b/core/Command/App/Install.php @@ -1,29 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Maxopoly <max@dermax.org> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author sualko <klaus@jsxc.org> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\App; @@ -36,7 +17,14 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Install extends Command { - protected function configure() { + public function __construct( + protected IAppManager $appManager, + private Installer $installer, + ) { + parent::__construct(); + } + + protected function configure(): void { $this ->setName('app:install') ->setDescription('install an app') @@ -68,34 +56,26 @@ class Install extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $appId = $input->getArgument('app-id'); - $forceEnable = (bool) $input->getOption('force'); + $forceEnable = (bool)$input->getOption('force'); - if (\OC_App::getAppPath($appId)) { + if ($this->appManager->isEnabledForAnyone($appId)) { $output->writeln($appId . ' already installed'); return 1; } try { - /** @var Installer $installer */ - $installer = \OC::$server->query(Installer::class); - $installer->downloadApp($appId, $input->getOption('allow-unstable')); - $result = $installer->installApp($appId, $forceEnable); + $this->installer->downloadApp($appId, $input->getOption('allow-unstable')); + $result = $this->installer->installApp($appId, $forceEnable); } catch (\Exception $e) { $output->writeln('Error: ' . $e->getMessage()); return 1; } - if ($result === false) { - $output->writeln($appId . ' couldn\'t be installed'); - return 1; - } - - $appVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId); + $appVersion = $this->appManager->getAppVersion($appId); $output->writeln($appId . ' ' . $appVersion . ' installed'); if (!$input->getOption('keep-disabled')) { - $appClass = new \OC_App(); - $appClass->enable($appId); + $this->appManager->enableApp($appId); $output->writeln($appId . ' enabled'); } diff --git a/core/Command/App/ListApps.php b/core/Command/App/ListApps.php index 24856304afc..dc947bea55f 100644 --- a/core/Command/App/ListApps.php +++ b/core/Command/App/ListApps.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\App; @@ -34,12 +15,12 @@ use Symfony\Component\Console\Output\OutputInterface; class ListApps extends Base { public function __construct( - protected IAppManager $manager, + protected IAppManager $appManager, ) { parent::__construct(); } - protected function configure() { + protected function configure(): void { parent::configure(); $this @@ -51,6 +32,18 @@ class ListApps extends Base { InputOption::VALUE_REQUIRED, 'true - limit to shipped apps only, false - limit to non-shipped apps only' ) + ->addOption( + 'enabled', + null, + InputOption::VALUE_NONE, + 'shows only enabled apps' + ) + ->addOption( + 'disabled', + null, + InputOption::VALUE_NONE, + 'shows only disabled apps' + ) ; } @@ -61,32 +54,43 @@ class ListApps extends Base { $shippedFilter = null; } - $apps = \OC_App::getAllApps(); + $showEnabledApps = $input->getOption('enabled') || !$input->getOption('disabled'); + $showDisabledApps = $input->getOption('disabled') || !$input->getOption('enabled'); + + $apps = $this->appManager->getAllAppsInAppsFolders(); $enabledApps = $disabledApps = []; - $versions = \OC_App::getAppVersions(); + $versions = $this->appManager->getAppInstalledVersions(); //sort enabled apps above disabled apps foreach ($apps as $app) { - if ($shippedFilter !== null && $this->manager->isShipped($app) !== $shippedFilter) { + if ($shippedFilter !== null && $this->appManager->isShipped($app) !== $shippedFilter) { continue; } - if ($this->manager->isInstalled($app)) { + if ($this->appManager->isEnabledForAnyone($app)) { $enabledApps[] = $app; } else { $disabledApps[] = $app; } } - $apps = ['enabled' => [], 'disabled' => []]; + $apps = []; + + if ($showEnabledApps) { + $apps['enabled'] = []; - sort($enabledApps); - foreach ($enabledApps as $app) { - $apps['enabled'][$app] = $versions[$app] ?? true; + sort($enabledApps); + foreach ($enabledApps as $app) { + $apps['enabled'][$app] = $versions[$app] ?? true; + } } - sort($disabledApps); - foreach ($disabledApps as $app) { - $apps['disabled'][$app] = $this->manager->getAppVersion($app) . (isset($versions[$app]) ? ' (installed ' . $versions[$app] . ')' : ''); + if ($showDisabledApps) { + $apps['disabled'] = []; + + sort($disabledApps); + foreach ($disabledApps as $app) { + $apps['disabled'][$app] = $this->appManager->getAppVersion($app) . (isset($versions[$app]) ? ' (installed ' . $versions[$app] . ')' : ''); + } } $this->writeAppList($input, $output, $apps); @@ -98,14 +102,18 @@ 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: - $output->writeln('Enabled:'); - parent::writeArrayInOutputFormat($input, $output, $items['enabled']); - - $output->writeln('Disabled:'); - parent::writeArrayInOutputFormat($input, $output, $items['disabled']); + if (isset($items['enabled'])) { + $output->writeln('Enabled:'); + parent::writeArrayInOutputFormat($input, $output, $items['enabled']); + } + + if (isset($items['disabled'])) { + $output->writeln('Disabled:'); + parent::writeArrayInOutputFormat($input, $output, $items['disabled']); + } break; default: @@ -119,7 +127,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']; } @@ -131,7 +139,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 522ddc20e07..d43bfa96ccc 100644 --- a/core/Command/App/Remove.php +++ b/core/Command/App/Remove.php @@ -1,28 +1,10 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2018, Patrik Kernstock <info@pkern.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Patrik Kernstock <info@pkern.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\App; @@ -47,7 +29,7 @@ class Remove extends Command implements CompletionAwareInterface { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('app:remove') ->setDescription('remove an app') @@ -67,9 +49,9 @@ class Remove extends Command implements CompletionAwareInterface { protected function execute(InputInterface $input, OutputInterface $output): int { $appId = $input->getArgument('app-id'); - // Check if the app is installed - if (!\OC_App::getAppPath($appId)) { - $output->writeln($appId . ' is not installed'); + // Check if the app is enabled + if (!$this->manager->isEnabledForAnyone($appId)) { + $output->writeln($appId . ' is not enabled'); return 1; } @@ -124,7 +106,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 []; } @@ -133,9 +115,9 @@ 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(); + return $this->manager->getEnabledApps(); } return []; } diff --git a/core/Command/App/Update.php b/core/Command/App/Update.php index 7d47034398f..71c7f84e5b0 100644 --- a/core/Command/App/Update.php +++ b/core/Command/App/Update.php @@ -1,33 +1,15 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2018, michag86 (michag86@arcor.de) - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author michag86 <micha_g@arcor.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\App; use OC\Installer; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; @@ -45,7 +27,7 @@ class Update extends Command { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('app:update') ->setDescription('update an app or all apps') @@ -77,19 +59,20 @@ class Update extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $singleAppId = $input->getArgument('app-id'); + $updateFound = false; if ($singleAppId) { $apps = [$singleAppId]; try { $this->manager->getAppPath($singleAppId); - } catch (\OCP\App\AppPathNotFoundException $e) { + } catch (AppPathNotFoundException $e) { $output->writeln($singleAppId . ' not installed'); return 1; } } elseif ($input->getOption('all') || $input->getOption('showonly')) { - $apps = \OC_App::getAllApps(); + $apps = $this->manager->getAllAppsInAppsFolders(); } else { - $output->writeln("<error>Please specify an app to update or \"--all\" to update all updatable apps\"</error>"); + $output->writeln('<error>Please specify an app to update or "--all" to update all updatable apps"</error>'); return 1; } @@ -97,6 +80,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')) { @@ -115,13 +99,21 @@ class Update extends Command { if ($result === false) { $output->writeln($appId . ' couldn\'t be updated'); $return = 1; - } elseif ($result === true) { + } else { $output->writeln($appId . ' updated'); } } } } + 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 deleted file mode 100644 index 5dc94d939d7..00000000000 --- a/core/Command/Background/Ajax.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * The MIT License (MIT) - * - * Copyright (c) 2015 Christian Kampka <christian@kampka.net> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -namespace OC\Core\Command\Background; - -class Ajax extends Base { - protected function getMode() { - return 'ajax'; - } -} diff --git a/core/Command/Background/Base.php b/core/Command/Background/Base.php deleted file mode 100644 index 5b2da21af75..00000000000 --- a/core/Command/Background/Base.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php -/** - * The MIT License (MIT) - * - * Copyright (c) 2015 Christian Kampka <christian@kampka.net> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -namespace OC\Core\Command\Background; - -use OCP\IConfig; - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * An abstract base class for configuring the background job mode - * from the command line interface. - * Subclasses will override the getMode() function to specify the mode to configure. - */ -abstract class Base extends Command { - abstract protected function getMode(); - - public function __construct( - protected IConfig $config, - ) { - parent::__construct(); - } - - protected function configure() { - $mode = $this->getMode(); - $this - ->setName("background:$mode") - ->setDescription("Use $mode to run background jobs"); - } - - /** - * Executing this command will set the background job mode for owncloud. - * The mode to set is specified by the concrete sub class by implementing the - * getMode() function. - * - * @param InputInterface $input - * @param OutputInterface $output - */ - protected function execute(InputInterface $input, OutputInterface $output): int { - $mode = $this->getMode(); - $this->config->setAppValue('core', 'backgroundjobs_mode', $mode); - $output->writeln("Set mode for background jobs to '$mode'"); - return 0; - } -} diff --git a/core/Command/Background/Cron.php b/core/Command/Background/Cron.php deleted file mode 100644 index 9dbb4f855e5..00000000000 --- a/core/Command/Background/Cron.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * The MIT License (MIT) - * - * Copyright (c) 2015 Christian Kampka <christian@kampka.net> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -namespace OC\Core\Command\Background; - -class Cron extends Base { - protected function getMode() { - return 'cron'; - } -} diff --git a/core/Command/Background/Delete.php b/core/Command/Background/Delete.php index 8d57d18dfd0..50ae309065b 100644 --- a/core/Command/Background/Delete.php +++ b/core/Command/Background/Delete.php @@ -2,31 +2,15 @@ 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/>. - * + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Background; use OC\Core\Command\Base; use OCP\BackgroundJob\IJobList; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -51,7 +35,7 @@ class Delete extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $jobId = (int) $input->getArgument('job-id'); + $jobId = (int)$input->getArgument('job-id'); $job = $this->jobList->getById($jobId); if ($job === null) { @@ -68,6 +52,7 @@ class Delete extends Base { '/^(y|Y)/i' ); + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if (!$helper->ask($input, $output, $question)) { $output->writeln('aborted.'); diff --git a/core/Command/Background/Job.php b/core/Command/Background/Job.php index aeee60bd6a5..9a862f5a13a 100644 --- a/core/Command/Background/Job.php +++ b/core/Command/Background/Job.php @@ -2,32 +2,16 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021, 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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Background; use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; -use OCP\ILogger; +use OCP\BackgroundJob\QueuedJob; +use OCP\BackgroundJob\TimedJob; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -37,7 +21,6 @@ use Symfony\Component\Console\Output\OutputInterface; class Job extends Command { public function __construct( protected IJobList $jobList, - protected ILogger $logger, ) { parent::__construct(); } @@ -61,7 +44,7 @@ class Job extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $jobId = (int) $input->getArgument('job-id'); + $jobId = (int)$input->getArgument('job-id'); $job = $this->jobList->getById($jobId); if ($job === null) { @@ -86,14 +69,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 TimedJob) { $this->printJobInfo($jobId, $job, $output); } } else { @@ -104,23 +88,23 @@ 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(); - $lastRun->setTimestamp((int) $row['last_run']); + $lastRun->setTimestamp((int)$row['last_run']); $lastChecked = new \DateTime(); - $lastChecked->setTimestamp((int) $row['last_checked']); + $lastChecked->setTimestamp((int)$row['last_checked']); $reservedAt = new \DateTime(); - $reservedAt->setTimestamp((int) $row['reserved_at']); + $reservedAt->setTimestamp((int)$row['reserved_at']); $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 TimedJob; if ($isTimedJob) { $output->writeln('Type: timed'); - } elseif ($job instanceof \OC\BackgroundJob\QueuedJob || $job instanceof \OCP\BackgroundJob\QueuedJob) { + } elseif ($job instanceof QueuedJob) { $output->writeln('Type: queued'); } else { $output->writeln('Type: job'); @@ -128,7 +112,7 @@ class Job extends Command { $output->writeln(''); $output->writeln('Last checked: ' . $lastChecked->format(\DateTimeInterface::ATOM)); - if ((int) $row['reserved_at'] === 0) { + if ((int)$row['reserved_at'] === 0) { $output->writeln('Reserved at: -'); } else { $output->writeln('Reserved at: <comment>' . $reservedAt->format(\DateTimeInterface::ATOM) . '</comment>'); diff --git a/core/Command/Background/JobBase.php b/core/Command/Background/JobBase.php new file mode 100644 index 00000000000..81d16f874eb --- /dev/null +++ b/core/Command/Background/JobBase.php @@ -0,0 +1,82 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +namespace OC\Core\Command\Background; + +use OC\Core\Command\Base; +use OCP\BackgroundJob\IJob; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\QueuedJob; +use OCP\BackgroundJob\TimedJob; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Output\OutputInterface; + +abstract class JobBase extends Base { + + public function __construct( + protected IJobList $jobList, + protected LoggerInterface $logger, + ) { + parent::__construct(); + } + + protected function printJobInfo(int $jobId, IJob $job, OutputInterface $output): void { + $row = $this->jobList->getDetailsById($jobId); + + if ($row === null) { + return; + } + + $lastRun = new \DateTime(); + $lastRun->setTimestamp((int)$row['last_run']); + $lastChecked = new \DateTime(); + $lastChecked->setTimestamp((int)$row['last_checked']); + $reservedAt = new \DateTime(); + $reservedAt->setTimestamp((int)$row['reserved_at']); + + $output->writeln('Job class: ' . get_class($job)); + $output->writeln('Arguments: ' . json_encode($job->getArgument())); + + $isTimedJob = $job instanceof TimedJob; + if ($isTimedJob) { + $output->writeln('Type: timed'); + } elseif ($job instanceof QueuedJob) { + $output->writeln('Type: queued'); + } else { + $output->writeln('Type: job'); + } + + $output->writeln(''); + $output->writeln('Last checked: ' . $lastChecked->format(\DateTimeInterface::ATOM)); + if ((int)$row['reserved_at'] === 0) { + $output->writeln('Reserved at: -'); + } else { + $output->writeln('Reserved at: <comment>' . $reservedAt->format(\DateTimeInterface::ATOM) . '</comment>'); + } + $output->writeln('Last executed: ' . $lastRun->format(\DateTimeInterface::ATOM)); + $output->writeln('Last duration: ' . $row['execution_duration']); + + if ($isTimedJob) { + $reflection = new \ReflectionClass($job); + $intervalProperty = $reflection->getProperty('interval'); + $intervalProperty->setAccessible(true); + $interval = $intervalProperty->getValue($job); + + $nextRun = new \DateTime(); + $nextRun->setTimestamp((int)$row['last_run'] + $interval); + + if ($nextRun > new \DateTime()) { + $output->writeln('Next execution: <comment>' . $nextRun->format(\DateTimeInterface::ATOM) . '</comment>'); + } else { + $output->writeln('Next execution: <info>' . $nextRun->format(\DateTimeInterface::ATOM) . '</info>'); + } + } + } +} diff --git a/core/Command/Background/JobWorker.php b/core/Command/Background/JobWorker.php new file mode 100644 index 00000000000..8289021887b --- /dev/null +++ b/core/Command/Background/JobWorker.php @@ -0,0 +1,176 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Background; + +use OC\Core\Command\InterruptedException; +use OC\Files\SetupManager; +use OCP\BackgroundJob\IJobList; +use OCP\ITempManager; +use Psr\Log\LoggerInterface; +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 JobWorker extends JobBase { + + public function __construct( + protected IJobList $jobList, + protected LoggerInterface $logger, + private ITempManager $tempManager, + private SetupManager $setupManager, + ) { + parent::__construct($jobList, $logger); + } + protected function configure(): void { + parent::configure(); + + $this + ->setName('background-job:worker') + ->setDescription('Run a background job worker') + ->addArgument( + 'job-classes', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'The classes of the jobs to look for in the database' + ) + ->addOption( + 'once', + null, + InputOption::VALUE_NONE, + 'Only execute the worker once (as a regular cron execution would do it)' + ) + ->addOption( + 'interval', + 'i', + InputOption::VALUE_OPTIONAL, + 'Interval in seconds in which the worker should repeat already processed jobs (set to 0 for no repeat)', + 5 + ) + ->addOption( + 'stop_after', + 't', + InputOption::VALUE_OPTIONAL, + 'Duration after which the worker should stop and exit. The worker won\'t kill a potential running job, it will exit after this job has finished running (supported values are: "30" or "30s" for 30 seconds, "10m" for 10 minutes and "2h" for 2 hours)' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $startTime = time(); + $stopAfterOptionValue = $input->getOption('stop_after'); + $stopAfterSeconds = $stopAfterOptionValue === null + ? null + : $this->parseStopAfter($stopAfterOptionValue); + if ($stopAfterSeconds !== null) { + $output->writeln('<info>Background job worker will stop after ' . $stopAfterSeconds . ' seconds</info>'); + } + + $jobClasses = $input->getArgument('job-classes'); + $jobClasses = empty($jobClasses) ? null : $jobClasses; + + if ($jobClasses !== null) { + // at least one class is invalid + foreach ($jobClasses as $jobClass) { + if (!class_exists($jobClass)) { + $output->writeln('<error>Invalid job class: ' . $jobClass . '</error>'); + return 1; + } + } + } + + while (true) { + // Stop if we exceeded stop_after value + if ($stopAfterSeconds !== null && ($startTime + $stopAfterSeconds) < time()) { + $output->writeln('stop_after time has been exceeded, exiting...', OutputInterface::VERBOSITY_VERBOSE); + break; + } + // Handle canceling of the process + try { + $this->abortIfInterrupted(); + } catch (InterruptedException $e) { + $output->writeln('<info>Background job worker stopped</info>'); + break; + } + + $this->printSummary($input, $output); + + usleep(50000); + $job = $this->jobList->getNext(false, $jobClasses); + if (!$job) { + if ($input->getOption('once') === true) { + if ($jobClasses === null) { + $output->writeln('No job is currently queued', OutputInterface::VERBOSITY_VERBOSE); + } else { + $output->writeln('No job of classes [' . implode(', ', $jobClasses) . '] is currently queued', OutputInterface::VERBOSITY_VERBOSE); + } + $output->writeln('Exiting...', OutputInterface::VERBOSITY_VERBOSE); + break; + } + + $output->writeln('Waiting for new jobs to be queued', OutputInterface::VERBOSITY_VERBOSE); + // Re-check interval for new jobs + sleep(1); + continue; + } + + $output->writeln('Running job ' . get_class($job) . ' with ID ' . $job->getId()); + + if ($output->isVerbose()) { + $this->printJobInfo($job->getId(), $job, $output); + } + + /** @psalm-suppress DeprecatedMethod Calling execute until it is removed, then will switch to start */ + $job->execute($this->jobList); + + $output->writeln('Job ' . $job->getId() . ' has finished', OutputInterface::VERBOSITY_VERBOSE); + + // clean up after unclean jobs + $this->setupManager->tearDown(); + $this->tempManager->clean(); + + $this->jobList->setLastJob($job); + $this->jobList->unlockJob($job); + + if ($input->getOption('once') === true) { + break; + } + } + + return 0; + } + + private function printSummary(InputInterface $input, OutputInterface $output): void { + if (!$output->isVeryVerbose()) { + return; + } + $output->writeln('<comment>Summary</comment>'); + + $counts = []; + foreach ($this->jobList->countByClass() as $row) { + $counts[] = $row; + } + $this->writeTableInOutputFormat($input, $output, $counts); + } + + private function parseStopAfter(string $value): ?int { + if (is_numeric($value)) { + return (int)$value; + } + if (preg_match("/^(\d+)s$/i", $value, $matches)) { + return (int)$matches[0]; + } + if (preg_match("/^(\d+)m$/i", $value, $matches)) { + return 60 * ((int)$matches[0]); + } + if (preg_match("/^(\d+)h$/i", $value, $matches)) { + return 60 * 60 * ((int)$matches[0]); + } + return null; + } +} diff --git a/core/Command/Background/ListCommand.php b/core/Command/Background/ListCommand.php index ef5b478a197..c8efbfef5c7 100644 --- a/core/Command/Background/ListCommand.php +++ b/core/Command/Background/ListCommand.php @@ -2,25 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022, Côme Chilliet <come.chilliet@nextcloud.com> - * - * @author Côme Chilliet <come.chilliet@nextcloud.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/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Background; @@ -70,7 +53,7 @@ class ListCommand extends Base { $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>"); + $output->writeln("\n<comment>Output is currently limited to " . $limit . ' jobs. Specify `-l, --limit[=LIMIT]` to override.</comment>'); } return 0; } diff --git a/core/Command/Background/Mode.php b/core/Command/Background/Mode.php new file mode 100644 index 00000000000..4c0f40bb4a2 --- /dev/null +++ b/core/Command/Background/Mode.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2015 Christian Kampka <christian@kampka.net> + * SPDX-License-Identifier: MIT + */ +namespace OC\Core\Command\Background; + +use OCP\IAppConfig; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Mode extends Command { + public function __construct( + private IAppConfig $appConfig, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('background:cron') + ->setAliases(['background:ajax', 'background:webcron']) + ->setDescription('Use cron, ajax or webcron to run background jobs'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + /** @var 'background:cron'|'background:ajax'|'background:webcron' $command */ + $command = $input->getArgument('command'); + + $mode = match ($command) { + 'background:cron' => 'cron', + 'background:ajax' => 'ajax', + 'background:webcron' => 'webcron', + }; + + $this->appConfig->setValueString('core', 'backgroundjobs_mode', $mode); + $output->writeln("Set mode for background jobs to '" . $mode . "'"); + + return 0; + } +} diff --git a/core/Command/Background/WebCron.php b/core/Command/Background/WebCron.php deleted file mode 100644 index 7da379b6a53..00000000000 --- a/core/Command/Background/WebCron.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -/** - * The MIT License (MIT) - * - * Copyright (c) 2015 Christian Kampka <christian@kampka.net> - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -namespace OC\Core\Command\Background; - -class WebCron extends Base { - protected function getMode() { - return 'webcron'; - } -} diff --git a/core/Command/Base.php b/core/Command/Base.php index f8b864c5864..6ab2765b0f9 100644 --- a/core/Command/Base.php +++ b/core/Command/Base.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command; @@ -55,22 +37,24 @@ class Base extends Command implements CompletionAwareInterface { ; } - protected function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, array $items, string $prefix = ' - '): void { + protected function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, iterable $items, string $prefix = ' - '): void { switch ($input->getOption('output')) { case self::OUTPUT_FORMAT_JSON: + $items = (is_array($items) ? $items : iterator_to_array($items)); $output->writeln(json_encode($items)); break; case self::OUTPUT_FORMAT_JSON_PRETTY: + $items = (is_array($items) ? $items : iterator_to_array($items)); $output->writeln(json_encode($items, JSON_PRETTY_PRINT)); break; default: foreach ($items as $key => $item) { - if (is_array($item)) { + if (is_iterable($item)) { $output->writeln($prefix . $key . ':'); $this->writeArrayInOutputFormat($input, $output, $item, ' ' . $prefix); continue; } - if (!is_int($key) || ListCommand::class === get_class($this)) { + if (!is_int($key) || get_class($this) === ListCommand::class) { $value = $this->valueToString($item); if (!is_null($value)) { $output->writeln($prefix . $key . ': ' . $value); @@ -104,6 +88,58 @@ class Base extends Command implements CompletionAwareInterface { } } + protected function writeStreamingTableInOutputFormat(InputInterface $input, OutputInterface $output, \Iterator $items, int $tableGroupSize): void { + switch ($input->getOption('output')) { + case self::OUTPUT_FORMAT_JSON: + case self::OUTPUT_FORMAT_JSON_PRETTY: + $this->writeStreamingJsonArray($input, $output, $items); + break; + default: + foreach ($this->chunkIterator($items, $tableGroupSize) as $chunk) { + $this->writeTableInOutputFormat($input, $output, $chunk); + } + break; + } + } + + protected function writeStreamingJsonArray(InputInterface $input, OutputInterface $output, \Iterator $items): void { + $first = true; + $outputType = $input->getOption('output'); + + $output->writeln('['); + foreach ($items as $item) { + if (!$first) { + $output->writeln(','); + } + if ($outputType === self::OUTPUT_FORMAT_JSON_PRETTY) { + $output->write(json_encode($item, JSON_PRETTY_PRINT)); + } else { + $output->write(json_encode($item)); + } + $first = false; + } + $output->writeln("\n]"); + } + + public function chunkIterator(\Iterator $iterator, int $count): \Iterator { + $chunk = []; + + for ($i = 0; $iterator->valid(); $i++) { + $chunk[] = $iterator->current(); + $iterator->next(); + if (count($chunk) == $count) { + // Got a full chunk, yield and start a new one + yield $chunk; + $chunk = []; + } + } + + if (count($chunk)) { + // Yield the last chunk even if incomplete + yield $chunk; + } + } + /** * @param mixed $item @@ -134,6 +170,8 @@ class Base extends Command implements CompletionAwareInterface { return 'true'; } elseif ($value === null) { return $returnNull ? null : 'null'; + } if ($value instanceof \UnitEnum) { + return $value->value; } else { return $value; } @@ -165,7 +203,7 @@ class Base extends Command implements CompletionAwareInterface { $this->interrupted = true; } - public function run(InputInterface $input, OutputInterface $output) { + public function run(InputInterface $input, OutputInterface $output): int { // check if the php pcntl_signal functions are accessible $this->php_pcntl_signal = function_exists('pcntl_signal'); if ($this->php_pcntl_signal) { diff --git a/core/Command/Broadcast/Test.php b/core/Command/Broadcast/Test.php index 22a65d86e7b..eb8b49bc3ee 100644 --- a/core/Command/Broadcast/Test.php +++ b/core/Command/Broadcast/Test.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Broadcast; @@ -62,16 +44,11 @@ class Test extends Command { $uid = $input->getArgument('uid'); $event = new class($name, $uid) extends ABroadcastedEvent { - /** @var string */ - private $name; - /** @var string */ - private $uid; - - public function __construct(string $name, - string $uid) { + public function __construct( + private string $name, + private string $uid, + ) { parent::__construct(); - $this->name = $name; - $this->uid = $uid; } public function broadcastAs(): string { diff --git a/core/Command/Check.php b/core/Command/Check.php index 478486b7dee..dcc9b089e1c 100644 --- a/core/Command/Check.php +++ b/core/Command/Check.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command; @@ -48,7 +31,7 @@ class Check extends Base { $errors = \OC_Util::checkServer($this->config); if (!empty($errors)) { $errors = array_map(function ($item) { - return (string) $item['error']; + return (string)$item['error']; }, $errors); $this->writeArrayInOutputFormat($input, $output, $errors); diff --git a/core/Command/Config/App/Base.php b/core/Command/Config/App/Base.php index b40f7c9e48d..e90a8e78f5b 100644 --- a/core/Command/Config/App/Base.php +++ b/core/Command/Config/App/Base.php @@ -1,32 +1,23 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016 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/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Config\App; -use OCP\IConfig; +use OC\Config\ConfigManager; +use OCP\IAppConfig; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; abstract class Base extends \OC\Core\Command\Base { - protected IConfig $config; + public function __construct( + protected IAppConfig $appConfig, + protected readonly ConfigManager $configManager, + ) { + parent::__construct(); + } /** * @param string $argumentName @@ -35,12 +26,12 @@ abstract class Base extends \OC\Core\Command\Base { */ public function completeArgumentValues($argumentName, CompletionContext $context) { if ($argumentName === 'app') { - return \OC_App::getAllApps(); + return $this->appConfig->getApps(); } if ($argumentName === 'name') { $appName = $context->getWordAtIndex($context->getWordIndex() - 1); - return $this->config->getAppKeys($appName); + return $this->appConfig->getKeys($appName); } return []; } diff --git a/core/Command/Config/App/DeleteConfig.php b/core/Command/Config/App/DeleteConfig.php index b77f27ccd07..5a08ecbdc42 100644 --- a/core/Command/Config/App/DeleteConfig.php +++ b/core/Command/Config/App/DeleteConfig.php @@ -1,39 +1,19 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config\App; -use OCP\IConfig; 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 DeleteConfig extends Base { - public function __construct( - protected IConfig $config, - ) { - parent::__construct(); - } - protected function configure() { parent::configure(); @@ -63,12 +43,12 @@ class DeleteConfig extends Base { $appName = $input->getArgument('app'); $configName = $input->getArgument('name'); - if ($input->hasParameterOption('--error-if-not-exists') && !in_array($configName, $this->config->getAppKeys($appName))) { + if ($input->hasParameterOption('--error-if-not-exists') && !in_array($configName, $this->appConfig->getKeys($appName), true)) { $output->writeln('<error>Config ' . $configName . ' of app ' . $appName . ' could not be deleted because it did not exist</error>'); return 1; } - $this->config->deleteAppValue($appName, $configName); + $this->appConfig->deleteKey($appName, $configName); $output->writeln('<info>Config value ' . $configName . ' of app ' . $appName . ' deleted</info>'); return 0; } diff --git a/core/Command/Config/App/GetConfig.php b/core/Command/Config/App/GetConfig.php index f85f978cc61..af0c5648232 100644 --- a/core/Command/Config/App/GetConfig.php +++ b/core/Command/Config/App/GetConfig.php @@ -2,42 +2,19 @@ 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 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config\App; 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 { - public function __construct( - protected IAppConfig $appConfig, - ) { - parent::__construct(); - } - protected function configure() { parent::configure(); @@ -61,6 +38,12 @@ class GetConfig extends Base { 'returns complete details about the app config value' ) ->addOption( + '--key-details', + null, + InputOption::VALUE_NONE, + 'returns complete details about the app config key' + ) + ->addOption( 'default-value', null, InputOption::VALUE_OPTIONAL, @@ -72,7 +55,7 @@ class GetConfig extends Base { /** * Executes the current command. * - * @param InputInterface $input An InputInterface instance + * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance * @return int 0 if everything went fine, or an error code */ @@ -89,6 +72,12 @@ class GetConfig extends Base { return 0; } + if ($input->getOption('key-details')) { + $details = $this->appConfig->getKeyDetails($appName, $configName); + $this->writeArrayInOutputFormat($input, $output, $details); + return 0; + } + try { $configValue = $this->appConfig->getDetails($appName, $configName)['value']; } catch (AppConfigUnknownKeyException $e) { diff --git a/core/Command/Config/App/SetConfig.php b/core/Command/Config/App/SetConfig.php index aad31c85048..c818404fc0e 100644 --- a/core/Command/Config/App/SetConfig.php +++ b/core/Command/Config/App/SetConfig.php @@ -2,32 +2,16 @@ 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 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config\App; use OC\AppConfig; -use OCP\Exceptions\AppConfigIncorrectTypeException; use OCP\Exceptions\AppConfigUnknownKeyException; use OCP\IAppConfig; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -35,12 +19,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; class SetConfig extends Base { - public function __construct( - protected IAppConfig $appConfig, - ) { - parent::__construct(); - } - protected function configure() { parent::configure(); @@ -173,8 +151,8 @@ class SetConfig extends Base { */ $sensitive = $input->getOption('sensitive'); try { - $currSensitive = $this->appConfig->isLazy($appName, $configName); - if ($sensitive === null || $sensitive === $currSensitive || !$this->ask($input, $output, ($sensitive) ? 'LAZY' : 'NOT LAZY')) { + $currSensitive = $this->appConfig->isSensitive($appName, $configName, null); + if ($sensitive === null || $sensitive === $currSensitive || !$this->ask($input, $output, ($sensitive) ? 'SENSITIVE' : 'NOT SENSITIVE')) { $sensitive = $currSensitive; } } catch (AppConfigUnknownKeyException) { @@ -182,7 +160,6 @@ class SetConfig extends Base { } $value = (string)$input->getOption('value'); - switch ($type) { case IAppConfig::VALUE_MIXED: $updated = $this->appConfig->setValueMixed($appName, $configName, $value, $lazy, $sensitive); @@ -193,34 +170,19 @@ class SetConfig extends Base { 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); + $updated = $this->appConfig->setValueInt($appName, $configName, $this->configManager->convertToInt($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); + $updated = $this->appConfig->setValueFloat($appName, $configName, $this->configManager->convertToFloat($value), $lazy, $sensitive); break; case IAppConfig::VALUE_BOOL: - if (strtolower($value) === 'true') { - $valueBool = true; - } elseif (strtolower($value) === 'false') { - $valueBool = false; - } else { - throw new AppConfigIncorrectTypeException('Value is not a boolean, please use \'true\' or \'false\''); - } - $updated = $this->appConfig->setValueBool($appName, $configName, $valueBool, $lazy); + $updated = $this->appConfig->setValueBool($appName, $configName, $this->configManager->convertToBool($value), $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); + $updated = $this->appConfig->setValueArray($appName, $configName, $this->configManager->convertToArray($value), $lazy, $sensitive); break; } } @@ -232,11 +194,16 @@ class SetConfig extends Base { "<info>Config value '%s' for app '%s' is now set to '%s', stored as %s in %s</info>", $configName, $appName, - $current['value'], + $current['sensitive'] ? '<sensitive>' : $current['value'], $current['typeString'], $current['lazy'] ? 'lazy cache' : 'fast cache' ) ); + $keyDetails = $this->appConfig->getKeyDetails($appName, $configName); + if (($keyDetails['note'] ?? '') !== '') { + $output->writeln('<comment>Note:</comment> ' . $keyDetails['note']); + } + } else { $output->writeln('<info>Config value were not updated</info>'); } @@ -245,6 +212,7 @@ class SetConfig extends Base { } private function ask(InputInterface $input, OutputInterface $output, string $request): bool { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); if ($input->getOption('no-interaction')) { return true; diff --git a/core/Command/Config/Import.php b/core/Command/Config/Import.php index b8431c8c295..b58abec3390 100644 --- a/core/Command/Config/Import.php +++ b/core/Command/Config/Import.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config; @@ -65,7 +49,7 @@ class Import extends Command implements CompletionAwareInterface { try { $configs = $this->validateFileContent($content); } catch (\UnexpectedValueException $e) { - $output->writeln('<error>' . $e->getMessage(). '</error>'); + $output->writeln('<error>' . $e->getMessage() . '</error>'); return 1; } diff --git a/core/Command/Config/ListConfigs.php b/core/Command/Config/ListConfigs.php index 51a17eb2097..a7c195276eb 100644 --- a/core/Command/Config/ListConfigs.php +++ b/core/Command/Config/ListConfigs.php @@ -1,27 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config; +use OC\Config\ConfigManager; use OC\Core\Command\Base; use OC\SystemConfig; use OCP\IAppConfig; @@ -37,6 +23,7 @@ class ListConfigs extends Base { public function __construct( protected SystemConfig $systemConfig, protected IAppConfig $appConfig, + protected ConfigManager $configManager, ) { parent::__construct(); } @@ -59,6 +46,7 @@ class ListConfigs extends Base { InputOption::VALUE_NONE, 'Use this option when you want to include sensitive configs like passwords, salts, ...' ) + ->addOption('migrate', null, InputOption::VALUE_NONE, 'Rename config keys of all enabled apps, based on ConfigLexicon') ; } @@ -66,6 +54,10 @@ class ListConfigs extends Base { $app = $input->getArgument('app'); $noSensitiveValues = !$input->getOption('private'); + if ($input->getOption('migrate')) { + $this->configManager->migrateConfigLexiconKeys(($app === 'all') ? null : $app); + } + if (!is_string($app)) { $output->writeln('<error>Invalid app value given</error>'); return 1; @@ -133,7 +125,7 @@ class ListConfigs extends Base { */ protected function getAppConfigs(string $app, bool $noSensitiveValues) { if ($noSensitiveValues) { - return $this->appConfig->getFilteredValues($app, false); + return $this->appConfig->getFilteredValues($app); } else { return $this->appConfig->getValues($app, false); } diff --git a/core/Command/Config/Preset.php b/core/Command/Config/Preset.php new file mode 100644 index 00000000000..ebd8aaa5cdf --- /dev/null +++ b/core/Command/Config/Preset.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Config; + +use OC\Config\PresetManager; +use OC\Core\Command\Base; +use OCP\Config\Lexicon\Preset as ConfigLexiconPreset; +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 Preset extends Base { + public function __construct( + private readonly PresetManager $presetManager, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + $this->setName('config:preset') + ->setDescription('Select a config preset') + ->addArgument('preset', InputArgument::OPTIONAL, 'Preset to use for all unset config values', '') + ->addOption('list', '', InputOption::VALUE_NONE, 'display available preset'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($input->getOption('list')) { + $this->getEnum('', $list); + $this->writeArrayInOutputFormat($input, $output, $list); + return self::SUCCESS; + } + + $presetArg = $input->getArgument('preset'); + if ($presetArg !== '') { + $preset = $this->getEnum($presetArg, $list); + if ($preset === null) { + $output->writeln('<error>Invalid preset: ' . $presetArg . '</error>'); + $output->writeln('Available presets: ' . implode(', ', $list)); + return self::INVALID; + } + + $this->presetManager->setLexiconPreset($preset); + } + + $current = $this->presetManager->getLexiconPreset(); + $this->writeArrayInOutputFormat($input, $output, [$current->name], 'current preset: '); + return self::SUCCESS; + } + + private function getEnum(string $name, ?array &$list = null): ?ConfigLexiconPreset { + $list = []; + foreach (ConfigLexiconPreset::cases() as $case) { + $list[] = $case->name; + if (strtolower($case->name) === strtolower($name)) { + return $case; + } + } + + return null; + } +} diff --git a/core/Command/Config/System/Base.php b/core/Command/Config/System/Base.php index 09ec456f6a4..088d902b4fd 100644 --- a/core/Command/Config/System/Base.php +++ b/core/Command/Config/System/Base.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 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/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Config\System; diff --git a/core/Command/Config/System/CastHelper.php b/core/Command/Config/System/CastHelper.php new file mode 100644 index 00000000000..f2b838bdf9b --- /dev/null +++ b/core/Command/Config/System/CastHelper.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Config\System; + +class CastHelper { + /** + * @return array{value: mixed, readable-value: string} + */ + public function castValue(?string $value, string $type): array { + switch ($type) { + case 'integer': + case 'int': + if (!is_numeric($value)) { + throw new \InvalidArgumentException('Non-numeric value specified'); + } + return [ + 'value' => (int)$value, + 'readable-value' => 'integer ' . (int)$value, + ]; + + case 'double': + case 'float': + if (!is_numeric($value)) { + throw new \InvalidArgumentException('Non-numeric value specified'); + } + return [ + 'value' => (float)$value, + 'readable-value' => 'double ' . (float)$value, + ]; + + case 'boolean': + case 'bool': + $value = strtolower($value); + return match ($value) { + 'true' => [ + 'value' => true, + 'readable-value' => 'boolean ' . $value, + ], + 'false' => [ + 'value' => false, + 'readable-value' => 'boolean ' . $value, + ], + default => throw new \InvalidArgumentException('Unable to parse value as boolean'), + }; + + case 'null': + return [ + 'value' => null, + 'readable-value' => 'null', + ]; + + case 'string': + $value = (string)$value; + return [ + 'value' => $value, + 'readable-value' => ($value === '') ? 'empty string' : 'string ' . $value, + ]; + + case 'json': + $value = json_decode($value, true); + return [ + 'value' => $value, + 'readable-value' => 'json ' . json_encode($value), + ]; + + default: + throw new \InvalidArgumentException('Invalid type'); + } + } +} diff --git a/core/Command/Config/System/DeleteConfig.php b/core/Command/Config/System/DeleteConfig.php index f6650e7d6d3..03960136f6f 100644 --- a/core/Command/Config/System/DeleteConfig.php +++ b/core/Command/Config/System/DeleteConfig.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config\System; diff --git a/core/Command/Config/System/GetConfig.php b/core/Command/Config/System/GetConfig.php index ab5b884fda4..c0a9623a84e 100644 --- a/core/Command/Config/System/GetConfig.php +++ b/core/Command/Config/System/GetConfig.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config\System; @@ -58,7 +43,7 @@ class GetConfig extends Base { /** * Executes the current command. * - * @param InputInterface $input An InputInterface instance + * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance * @return int 0 if everything went fine, or an error code */ diff --git a/core/Command/Config/System/SetConfig.php b/core/Command/Config/System/SetConfig.php index ba5265a84d7..1b1bdc66a6e 100644 --- a/core/Command/Config/System/SetConfig.php +++ b/core/Command/Config/System/SetConfig.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Config\System; @@ -34,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; class SetConfig extends Base { public function __construct( SystemConfig $systemConfig, + private CastHelper $castHelper, ) { parent::__construct($systemConfig); } @@ -74,7 +58,7 @@ class SetConfig extends Base { protected function execute(InputInterface $input, OutputInterface $output): int { $configNames = $input->getArgument('name'); $configName = $configNames[0]; - $configValue = $this->castValue($input->getOption('value'), $input->getOption('type')); + $configValue = $this->castHelper->castValue($input->getOption('value'), $input->getOption('type')); $updateOnly = $input->getOption('update-only'); if (count($configNames) > 1) { @@ -98,73 +82,6 @@ class SetConfig extends Base { } /** - * @param string $value - * @param string $type - * @return mixed - * @throws \InvalidArgumentException - */ - protected function castValue($value, $type) { - switch ($type) { - case 'integer': - case 'int': - if (!is_numeric($value)) { - throw new \InvalidArgumentException('Non-numeric value specified'); - } - return [ - 'value' => (int) $value, - 'readable-value' => 'integer ' . (int) $value, - ]; - - case 'double': - case 'float': - if (!is_numeric($value)) { - throw new \InvalidArgumentException('Non-numeric value specified'); - } - return [ - 'value' => (double) $value, - 'readable-value' => 'double ' . (double) $value, - ]; - - case 'boolean': - case 'bool': - $value = strtolower($value); - switch ($value) { - case 'true': - return [ - 'value' => true, - 'readable-value' => 'boolean ' . $value, - ]; - - case 'false': - return [ - 'value' => false, - 'readable-value' => 'boolean ' . $value, - ]; - - default: - throw new \InvalidArgumentException('Unable to parse value as boolean'); - } - - // no break - case 'null': - return [ - 'value' => null, - 'readable-value' => 'null', - ]; - - case 'string': - $value = (string) $value; - return [ - 'value' => $value, - 'readable-value' => ($value === '') ? 'empty string' : 'string ' . $value, - ]; - - default: - throw new \InvalidArgumentException('Invalid type'); - } - } - - /** * @param array $configNames * @param mixed $existingValues * @param mixed $value @@ -200,7 +117,7 @@ class SetConfig extends Base { */ public function completeOptionValues($optionName, CompletionContext $context) { if ($optionName === 'type') { - return ['string', 'integer', 'double', 'boolean']; + return ['string', 'integer', 'double', 'boolean', 'json', 'null']; } return parent::completeOptionValues($optionName, $context); } diff --git a/core/Command/Db/AddMissingColumns.php b/core/Command/Db/AddMissingColumns.php index 07763c66154..33b4b24a6cb 100644 --- a/core/Command/Db/AddMissingColumns.php +++ b/core/Command/Db/AddMissingColumns.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; @@ -55,7 +37,7 @@ class AddMissingColumns extends Command { $this ->setName('db:add-missing-columns') ->setDescription('Add missing optional columns to the database tables') - ->addOption('dry-run', null, InputOption::VALUE_NONE, "Output the SQL queries instead of running them."); + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the SQL queries instead of running them.'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/core/Command/Db/AddMissingIndices.php b/core/Command/Db/AddMissingIndices.php index bd47fea20fe..eec0aedce11 100644 --- a/core/Command/Db/AddMissingIndices.php +++ b/core/Command/Db/AddMissingIndices.php @@ -3,33 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Mario Danic <mario@lovelyhq.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; @@ -62,7 +37,7 @@ class AddMissingIndices extends Command { $this ->setName('db:add-missing-indices') ->setDescription('Add missing indices to the database tables') - ->addOption('dry-run', null, InputOption::VALUE_NONE, "Output the SQL queries instead of running them."); + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the SQL queries instead of running them.'); } protected function execute(InputInterface $input, OutputInterface $output): int { @@ -73,7 +48,9 @@ class AddMissingIndices extends Command { $this->dispatcher->dispatchTyped($event); $missingIndices = $event->getMissingIndices(); - if ($missingIndices !== []) { + $toReplaceIndices = $event->getIndicesToReplace(); + + if ($missingIndices !== [] || $toReplaceIndices !== []) { $schema = new SchemaWrapper($this->connection); foreach ($missingIndices as $missingIndex) { @@ -97,15 +74,54 @@ class AddMissingIndices extends Command { $table->addIndex($missingIndex['columns'], $missingIndex['indexName'], [], $missingIndex['options']); } - - $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>'); } } } + + foreach ($toReplaceIndices as $toReplaceIndex) { + if ($schema->hasTable($toReplaceIndex['tableName'])) { + $table = $schema->getTable($toReplaceIndex['tableName']); + + if ($table->hasIndex($toReplaceIndex['newIndexName'])) { + continue; + } + + $output->writeln('<info>Adding additional ' . $toReplaceIndex['newIndexName'] . ' index to the ' . $table->getName() . ' table, this can take some time...</info>'); + + if ($toReplaceIndex['uniqueIndex']) { + $table->addUniqueIndex($toReplaceIndex['columns'], $toReplaceIndex['newIndexName'], $toReplaceIndex['options']); + } else { + $table->addIndex($toReplaceIndex['columns'], $toReplaceIndex['newIndexName'], [], $toReplaceIndex['options']); + } + + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); + } + + foreach ($toReplaceIndex['oldIndexNames'] as $oldIndexName) { + if ($table->hasIndex($oldIndexName)) { + $output->writeln('<info>Removing ' . $oldIndexName . ' index from the ' . $table->getName() . ' table</info>'); + $table->dropIndex($oldIndexName); + } + } + + if (!$dryRun) { + $this->connection->migrateToSchema($schema->getWrappedSchema()); + } + $output->writeln('<info>' . $table->getName() . ' table updated successfully.</info>'); + } + } + + if ($dryRun) { + $sqlQueries = $this->connection->migrateToSchema($schema->getWrappedSchema(), $dryRun); + if ($sqlQueries !== null) { + $output->writeln($sqlQueries); + } + } } return 0; diff --git a/core/Command/Db/AddMissingPrimaryKeys.php b/core/Command/Db/AddMissingPrimaryKeys.php index 658eb0b0f5a..1eb11c894fa 100644 --- a/core/Command/Db/AddMissingPrimaryKeys.php +++ b/core/Command/Db/AddMissingPrimaryKeys.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; @@ -55,7 +37,7 @@ class AddMissingPrimaryKeys extends Command { $this ->setName('db:add-missing-primary-keys') ->setDescription('Add missing primary keys to the database tables') - ->addOption('dry-run', null, InputOption::VALUE_NONE, "Output the SQL queries instead of running them."); + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Output the SQL queries instead of running them.'); } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/core/Command/Db/ConvertFilecacheBigInt.php b/core/Command/Db/ConvertFilecacheBigInt.php index 44cd81cd7eb..0d96d139701 100644 --- a/core/Command/Db/ConvertFilecacheBigInt.php +++ b/core/Command/Db/ConvertFilecacheBigInt.php @@ -1,42 +1,18 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * - * @author Alecks Gates <alecks.g@gmail.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author timm2k <timm2k@gmx.de> - * @author Vincent Petry <vincent@nextcloud.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/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Db; -use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\Types\Type; use OC\DB\Connection; use OC\DB\SchemaWrapper; use OCP\DB\Types; +use OCP\IDBConnection; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; @@ -79,7 +55,7 @@ class ConvertFilecacheBigInt extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $schema = new SchemaWrapper($this->connection); - $isSqlite = $this->connection->getDatabasePlatform() instanceof SqlitePlatform; + $isSqlite = $this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_SQLITE; $updates = []; $tables = static::getColumnsByTable(); @@ -115,6 +91,7 @@ class ConvertFilecacheBigInt extends Command { $output->writeln('<comment>This can take up to hours, depending on the number of files in your instance!</comment>'); if ($input->isInteractive()) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Continue with the conversion (y/n)? [n] ', false); diff --git a/core/Command/Db/ConvertMysqlToMB4.php b/core/Command/Db/ConvertMysqlToMB4.php index a66fb46fc51..926e56c4300 100644 --- a/core/Command/Db/ConvertMysqlToMB4.php +++ b/core/Command/Db/ConvertMysqlToMB4.php @@ -1,30 +1,11 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db; -use Doctrine\DBAL\Platforms\MySQLPlatform; use OC\DB\MySqlTools; use OC\Migration\ConsoleOutput; use OC\Repair\Collation; @@ -53,15 +34,15 @@ class ConvertMysqlToMB4 extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - if (!$this->connection->getDatabasePlatform() instanceof MySQLPlatform) { - $output->writeln("This command is only valid for MySQL/MariaDB databases."); + if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_MYSQL) { + $output->writeln('This command is only valid for MySQL/MariaDB databases.'); return 1; } $tools = new MySqlTools(); if (!$tools->supports4ByteCharset($this->connection)) { $url = $this->urlGenerator->linkToDocs('admin-mysql-utf8mb4'); - $output->writeln("The database is not properly setup to use the charset utf8mb4."); + $output->writeln('The database is not properly setup to use the charset utf8mb4.'); $output->writeln("For more information please read the documentation at $url"); return 1; } diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index a1bfbd1fd3e..0067bec4d9e 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -1,34 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bernhard Ostertag <bernieo.code@gmx.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Łukasz Buśko <busko.lukasz@pm.me> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sander Ruitenbeek <sander@grids.be> - * @author Simon Spannagel <simonspa@kth.se> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db; @@ -38,9 +13,12 @@ use Doctrine\DBAL\Schema\Table; use OC\DB\Connection; use OC\DB\ConnectionFactory; use OC\DB\MigrationService; +use OC\DB\PgSqlTools; +use OCP\App\IAppManager; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\Types; use OCP\IConfig; +use OCP\Server; use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Command\Command; @@ -61,6 +39,7 @@ class ConvertType extends Command implements CompletionAwareInterface { public function __construct( protected IConfig $config, protected ConnectionFactory $connectionFactory, + protected IAppManager $appManager, ) { parent::__construct(); } @@ -167,10 +146,13 @@ class ConvertType extends Command implements CompletionAwareInterface { if ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question('What is the database password?'); + $question = new Question('What is the database password (press <enter> for none)? '); $question->setHidden(true); $question->setHiddenFallback(false); $password = $helper->ask($input, $output, $question); + if ($password === null) { + $password = ''; // possibly unnecessary + } $input->setOption('password', $password); return; } @@ -181,7 +163,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $this->readPassword($input, $output); /** @var Connection $fromDB */ - $fromDB = \OC::$server->get(Connection::class); + $fromDB = Server::get(Connection::class); $toDB = $this->getToDBConnection($input, $output); if ($input->getOption('clear-schema')) { @@ -199,7 +181,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>'); } @@ -228,11 +210,13 @@ class ConvertType extends Command implements CompletionAwareInterface { $toMS->migrate($currentMigration); } - $apps = $input->getOption('all-apps') ? \OC_App::getAllApps() : \OC_App::getEnabledApps(); + $apps = $input->getOption('all-apps') + ? $this->appManager->getAllAppsInAppsFolders() + : $this->appManager->getEnabledApps(); foreach ($apps as $app) { - $output->writeln('<info> - '.$app.'</info>'); + $output->writeln('<info> - ' . $app . '</info>'); // Make sure autoloading works... - \OC_App::loadApp($app); + $this->appManager->loadApp($app); $fromMS = new MigrationService($app, $fromDB); $currentMigration = $fromMS->getMigration('current'); if ($currentMigration !== '0') { @@ -244,16 +228,31 @@ class ConvertType extends Command implements CompletionAwareInterface { protected function getToDBConnection(InputInterface $input, OutputInterface $output) { $type = $input->getArgument('type'); - $connectionParams = $this->connectionFactory->createConnectionParams(); + $connectionParams = $this->connectionFactory->createConnectionParams(type: $type); $connectionParams = array_merge($connectionParams, [ 'host' => $input->getArgument('hostname'), 'user' => $input->getArgument('username'), 'password' => $input->getOption('password'), 'dbname' => $input->getArgument('database'), ]); + + // parse port if ($input->getOption('port')) { $connectionParams['port'] = $input->getOption('port'); } + + // parse hostname for unix socket + if (preg_match('/^(.+)(:(\d+|[^:]+))?$/', $input->getArgument('hostname'), $matches)) { + $connectionParams['host'] = $matches[1]; + if (isset($matches[3])) { + if (is_numeric($matches[3])) { + $connectionParams['port'] = $matches[3]; + } else { + $connectionParams['unix_socket'] = $matches[3]; + } + } + } + return $this->connectionFactory->getConnection($type, $connectionParams); } @@ -263,7 +262,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $output->writeln('<info>Clearing schema in new database</info>'); } foreach ($toTables as $table) { - $db->getSchemaManager()->dropTable($table); + $db->createSchemaManager()->dropTable($table); } } @@ -276,7 +275,7 @@ class ConvertType extends Command implements CompletionAwareInterface { } return preg_match($filterExpression, $asset) !== false; }); - return $db->getSchemaManager()->listTableNames(); + return $db->createSchemaManager()->listTableNames(); } /** @@ -298,7 +297,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $query->automaticTablePrefix(false); $query->select($query->func()->count('*', 'num_entries')) ->from($table->getName()); - $result = $query->execute(); + $result = $query->executeQuery(); $count = $result->fetchOne(); $result->closeCursor(); @@ -337,7 +336,7 @@ class ConvertType extends Command implements CompletionAwareInterface { for ($chunk = 0; $chunk < $numChunks; $chunk++) { $query->setFirstResult($chunk * $chunkSize); - $result = $query->execute(); + $result = $query->executeQuery(); try { $toDB->beginTransaction(); @@ -404,11 +403,11 @@ class ConvertType extends Command implements CompletionAwareInterface { try { // copy table rows foreach ($tables as $table) { - $output->writeln('<info> - '.$table.'</info>'); + $output->writeln('<info> - ' . $table . '</info>'); $this->copyTable($fromDB, $toDB, $schema->getTable($table), $input, $output); } if ($input->getArgument('type') === 'pgsql') { - $tools = new \OC\DB\PgSqlTools($this->config); + $tools = new PgSqlTools($this->config); $tools->resynchronizeDatabaseSequences($toDB); } // save new database config @@ -427,7 +426,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $dbName = $input->getArgument('database'); $password = $input->getOption('password'); if ($input->getOption('port')) { - $dbHost .= ':'.$input->getOption('port'); + $dbHost .= ':' . $input->getOption('port'); } $this->config->setSystemValues([ diff --git a/core/Command/Db/ExpectedSchema.php b/core/Command/Db/ExpectedSchema.php new file mode 100644 index 00000000000..1f35daba089 --- /dev/null +++ b/core/Command/Db/ExpectedSchema.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Db; + +use Doctrine\DBAL\Schema\Schema; +use OC\Core\Command\Base; +use OC\DB\Connection; +use OC\DB\MigrationService; +use OC\DB\SchemaWrapper; +use OC\Migration\NullOutput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ExpectedSchema extends Base { + public function __construct( + protected Connection $connection, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('db:schema:expected') + ->setDescription('Export the expected database schema for a fresh installation') + ->setHelp("Note that the expected schema might not exactly match the exported live schema as the expected schema doesn't take into account any database wide settings or defaults.") + ->addOption('sql', null, InputOption::VALUE_NONE, 'Dump the SQL statements for creating the expected schema'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $schema = new Schema(); + + $this->applyMigrations('core', $schema); + + $apps = \OC_App::getEnabledApps(); + foreach ($apps as $app) { + $this->applyMigrations($app, $schema); + } + + $sql = $input->getOption('sql'); + if ($sql) { + $output->writeln($schema->toSql($this->connection->getDatabasePlatform())); + } else { + $encoder = new SchemaEncoder(); + $this->writeArrayInOutputFormat($input, $output, $encoder->encodeSchema($schema, $this->connection->getDatabasePlatform())); + } + + return 0; + } + + private function applyMigrations(string $app, Schema $schema): void { + $output = new NullOutput(); + $ms = new MigrationService($app, $this->connection, $output); + foreach ($ms->getAvailableVersions() as $version) { + $migration = $ms->createInstance($version); + $migration->changeSchema($output, function () use (&$schema) { + return new SchemaWrapper($this->connection, $schema); + }, []); + } + } +} diff --git a/core/Command/Db/ExportSchema.php b/core/Command/Db/ExportSchema.php new file mode 100644 index 00000000000..581824eea5f --- /dev/null +++ b/core/Command/Db/ExportSchema.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Db; + +use OC\Core\Command\Base; +use OCP\IDBConnection; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ExportSchema extends Base { + public function __construct( + protected IDBConnection $connection, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('db:schema:export') + ->setDescription('Export the current database schema') + ->addOption('sql', null, InputOption::VALUE_NONE, 'Dump the SQL statements for creating a copy of the schema'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $schema = $this->connection->createSchema(); + $sql = $input->getOption('sql'); + if ($sql) { + $output->writeln($schema->toSql($this->connection->getDatabasePlatform())); + } else { + $encoder = new SchemaEncoder(); + $this->writeArrayInOutputFormat($input, $output, $encoder->encodeSchema($schema, $this->connection->getDatabasePlatform())); + } + + return 0; + } +} diff --git a/core/Command/Db/Migrations/ExecuteCommand.php b/core/Command/Db/Migrations/ExecuteCommand.php index c75b575ab6c..a89072c1ad1 100644 --- a/core/Command/Db/Migrations/ExecuteCommand.php +++ b/core/Command/Db/Migrations/ExecuteCommand.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; diff --git a/core/Command/Db/Migrations/GenerateCommand.php b/core/Command/Db/Migrations/GenerateCommand.php index 879844dd579..a75280fa8b1 100644 --- a/core/Command/Db/Migrations/GenerateCommand.php +++ b/core/Command/Db/Migrations/GenerateCommand.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com> - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; @@ -33,37 +16,21 @@ use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareI use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Helper\QuestionHelper; 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 GenerateCommand extends Command implements CompletionAwareInterface { - protected static $_templateSimple = - '<?php + protected static $_templateSimple + = '<?php declare(strict_types=1); /** - * @copyright Copyright (c) {{year}} Your name <your@email.com> - * - * @author Your name <your@email.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/>. - * + * SPDX-FileCopyrightText: {{year}} Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace {{namespace}}; @@ -72,9 +39,10 @@ use Closure; use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; +use Override; /** - * Auto-generated migration step: Please modify to your needs! + * FIXME Auto-generated migration step: Please modify to your needs! */ class {{classname}} extends SimpleMigrationStep { @@ -83,6 +51,7 @@ class {{classname}} extends SimpleMigrationStep { * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ + #[Override] public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { } @@ -92,6 +61,7 @@ class {{classname}} extends SimpleMigrationStep { * @param array $options * @return null|ISchemaWrapper */ + #[Override] public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { {{schemabody}} } @@ -101,18 +71,16 @@ class {{classname}} extends SimpleMigrationStep { * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ + #[Override] public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { } } '; - protected Connection $connection; - protected IAppManager $appManager; - - public function __construct(Connection $connection, IAppManager $appManager) { - $this->connection = $connection; - $this->appManager = $appManager; - + public function __construct( + protected Connection $connection, + protected IAppManager $appManager, + ) { parent::__construct(); } @@ -147,7 +115,7 @@ class {{classname}} extends SimpleMigrationStep { if ($fullVersion) { [$major, $minor] = explode('.', $fullVersion); - $shouldVersion = (string) ((int)$major * 1000 + (int)$minor); + $shouldVersion = (string)((int)$major * 1000 + (int)$minor); if ($version !== $shouldVersion) { $output->writeln('<comment>Unexpected migration version for current version: ' . $fullVersion . '</comment>'); $output->writeln('<comment> - Pattern: XYYY </comment>'); @@ -155,6 +123,7 @@ class {{classname}} extends SimpleMigrationStep { $output->writeln('<comment> - Actual: ' . $version . '</comment>'); if ($input->isInteractive()) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Continue with your given version? (y/n) [n] ', false); @@ -190,7 +159,7 @@ class {{classname}} extends SimpleMigrationStep { */ public function completeArgumentValues($argumentName, CompletionContext $context) { if ($argumentName === 'app') { - $allApps = \OC_App::getAllApps(); + $allApps = $this->appManager->getAllAppsInAppsFolders(); return array_diff($allApps, \OC_App::getEnabledApps(true, true)); } diff --git a/core/Command/Db/Migrations/GenerateMetadataCommand.php b/core/Command/Db/Migrations/GenerateMetadataCommand.php new file mode 100644 index 00000000000..581259c99df --- /dev/null +++ b/core/Command/Db/Migrations/GenerateMetadataCommand.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Db\Migrations; + +use OC\Migration\MetadataManager; +use OCP\App\IAppManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @since 30.0.0 + */ +class GenerateMetadataCommand extends Command { + public function __construct( + private readonly MetadataManager $metadataManager, + private readonly IAppManager $appManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this->setName('migrations:generate-metadata') + ->setHidden(true) + ->setDescription('Generate metadata from DB migrations - internal and should not be used'); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln( + json_encode( + [ + 'migrations' => $this->extractMigrationMetadata() + ], + JSON_PRETTY_PRINT + ) + ); + + return 0; + } + + private function extractMigrationMetadata(): array { + return [ + 'core' => $this->extractMigrationMetadataFromCore(), + 'apps' => $this->extractMigrationMetadataFromApps() + ]; + } + + private function extractMigrationMetadataFromCore(): array { + return $this->metadataManager->extractMigrationAttributes('core'); + } + + /** + * get all apps and extract attributes + * + * @return array + * @throws \Exception + */ + private function extractMigrationMetadataFromApps(): array { + $allApps = $this->appManager->getAllAppsInAppsFolders(); + $metadata = []; + foreach ($allApps as $appId) { + // We need to load app before being able to extract Migrations + $alreadyLoaded = $this->appManager->isAppLoaded($appId); + if (!$alreadyLoaded) { + $this->appManager->loadApp($appId); + } + $metadata[$appId] = $this->metadataManager->extractMigrationAttributes($appId); + } + return $metadata; + } +} diff --git a/core/Command/Db/Migrations/MigrateCommand.php b/core/Command/Db/Migrations/MigrateCommand.php index 3e11b32665a..2e02f031479 100644 --- a/core/Command/Db/Migrations/MigrateCommand.php +++ b/core/Command/Db/Migrations/MigrateCommand.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; diff --git a/core/Command/Db/Migrations/PreviewCommand.php b/core/Command/Db/Migrations/PreviewCommand.php new file mode 100644 index 00000000000..f5b850fff76 --- /dev/null +++ b/core/Command/Db/Migrations/PreviewCommand.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Db\Migrations; + +use OC\Migration\MetadataManager; +use OC\Updater\ReleaseMetadata; +use OCP\Migration\Attributes\MigrationAttribute; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableCellStyle; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @since 30.0.0 + */ +class PreviewCommand extends Command { + private bool $initiated = false; + public function __construct( + private readonly MetadataManager $metadataManager, + private readonly ReleaseMetadata $releaseMetadata, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('migrations:preview') + ->setDescription('Get preview of available DB migrations in case of initiating an upgrade') + ->addArgument('version', InputArgument::REQUIRED, 'The destination version number'); + + parent::configure(); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $version = $input->getArgument('version'); + if (filter_var($version, FILTER_VALIDATE_URL)) { + $metadata = $this->releaseMetadata->downloadMetadata($version); + } elseif (str_starts_with($version, '/')) { + $metadata = json_decode(file_get_contents($version), true, flags: JSON_THROW_ON_ERROR); + } else { + $metadata = $this->releaseMetadata->getMetadata($version); + } + + $parsed = $this->metadataManager->getMigrationsAttributesFromReleaseMetadata($metadata['migrations'] ?? [], true); + + $table = new Table($output); + $this->displayMigrations($table, 'core', $parsed['core'] ?? []); + foreach ($parsed['apps'] as $appId => $migrations) { + if (!empty($migrations)) { + $this->displayMigrations($table, $appId, $migrations); + } + } + $table->render(); + + $unsupportedApps = $this->metadataManager->getUnsupportedApps($metadata['migrations']); + if (!empty($unsupportedApps)) { + $output->writeln(''); + $output->writeln('Those apps are not supporting metadata yet and might initiate migrations on upgrade: <info>' . implode(', ', $unsupportedApps) . '</info>'); + } + + return 0; + } + + private function displayMigrations(Table $table, string $appId, array $data): void { + if (empty($data)) { + return; + } + + if ($this->initiated) { + $table->addRow(new TableSeparator()); + } + $this->initiated = true; + + $table->addRow( + [ + new TableCell( + $appId, + [ + 'colspan' => 2, + 'style' => new TableCellStyle(['cellFormat' => '<info>%s</info>']) + ] + ) + ] + )->addRow(new TableSeparator()); + + /** @var MigrationAttribute[] $attributes */ + foreach ($data as $migration => $attributes) { + $attributesStr = []; + if (empty($attributes)) { + $attributesStr[] = '<comment>(metadata not set)</comment>'; + } + foreach ($attributes as $attribute) { + $definition = '<info>' . $attribute->definition() . '</info>'; + $definition .= empty($attribute->getDescription()) ? '' : "\n " . $attribute->getDescription(); + $definition .= empty($attribute->getNotes()) ? '' : "\n <comment>" . implode("</comment>\n <comment>", $attribute->getNotes()) . '</comment>'; + $attributesStr[] = $definition; + } + $table->addRow([$migration, implode("\n", $attributesStr)]); + } + } +} diff --git a/core/Command/Db/Migrations/StatusCommand.php b/core/Command/Db/Migrations/StatusCommand.php index 52bc51a169f..97ecc76a924 100644 --- a/core/Command/Db/Migrations/StatusCommand.php +++ b/core/Command/Db/Migrations/StatusCommand.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud GmbH - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Db\Migrations; diff --git a/core/Command/Db/SchemaEncoder.php b/core/Command/Db/SchemaEncoder.php new file mode 100644 index 00000000000..beae3a81264 --- /dev/null +++ b/core/Command/Db/SchemaEncoder.php @@ -0,0 +1,115 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Db; + +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Types\PhpIntegerMappingType; +use Doctrine\DBAL\Types\Type; + +class SchemaEncoder { + /** + * Encode a DBAL schema to json, performing some normalization based on the database platform + * + * @param Schema $schema + * @param AbstractPlatform $platform + * @return array + */ + public function encodeSchema(Schema $schema, AbstractPlatform $platform): array { + $encoded = ['tables' => [], 'sequences' => []]; + foreach ($schema->getTables() as $table) { + $encoded[$table->getName()] = $this->encodeTable($table, $platform); + } + ksort($encoded); + return $encoded; + } + + /** + * @psalm-type ColumnArrayType = + */ + private function encodeTable(Table $table, AbstractPlatform $platform): array { + $encoded = ['columns' => [], 'indexes' => []]; + foreach ($table->getColumns() as $column) { + /** + * @var array{ + * name: string, + * default: mixed, + * notnull: bool, + * length: ?int, + * precision: int, + * scale: int, + * unsigned: bool, + * fixed: bool, + * autoincrement: bool, + * comment: string, + * columnDefinition: ?string, + * collation?: string, + * charset?: string, + * jsonb?: bool, + * } $data + **/ + $data = $column->toArray(); + $data['type'] = Type::getTypeRegistry()->lookupName($column->getType()); + $data['default'] = $column->getType()->convertToPHPValue($column->getDefault(), $platform); + if ($platform instanceof PostgreSQLPlatform) { + $data['unsigned'] = false; + if ($column->getType() instanceof PhpIntegerMappingType) { + $data['length'] = null; + } + unset($data['jsonb']); + } elseif ($platform instanceof AbstractMySqlPlatform) { + if ($column->getType() instanceof PhpIntegerMappingType) { + $data['length'] = null; + } elseif (in_array($data['type'], ['text', 'blob', 'datetime', 'float', 'json'])) { + $data['length'] = 0; + } + unset($data['collation']); + unset($data['charset']); + } + if ($data['type'] === 'string' && $data['length'] === null) { + $data['length'] = 255; + } + $encoded['columns'][$column->getName()] = $data; + } + ksort($encoded['columns']); + foreach ($table->getIndexes() as $index) { + $options = $index->getOptions(); + if (isset($options['lengths']) && count(array_filter($options['lengths'])) === 0) { + unset($options['lengths']); + } + if ($index->isPrimary()) { + if ($platform instanceof PostgreSqlPlatform) { + $name = $table->getName() . '_pkey'; + } elseif ($platform instanceof AbstractMySQLPlatform) { + $name = 'PRIMARY'; + } else { + $name = $index->getName(); + } + } else { + $name = $index->getName(); + } + if ($platform instanceof PostgreSqlPlatform) { + $name = strtolower($name); + } + $encoded['indexes'][$name] = [ + 'name' => $name, + 'columns' => $index->getColumns(), + 'unique' => $index->isUnique(), + 'primary' => $index->isPrimary(), + 'flags' => $index->getFlags(), + 'options' => $options, + ]; + } + ksort($encoded['indexes']); + return $encoded; + } +} diff --git a/core/Command/Encryption/ChangeKeyStorageRoot.php b/core/Command/Encryption/ChangeKeyStorageRoot.php index 96884de25e9..3049fd2ca08 100644 --- a/core/Command/Encryption/ChangeKeyStorageRoot.php +++ b/core/Command/Encryption/ChangeKeyStorageRoot.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; @@ -97,10 +79,10 @@ class ChangeKeyStorageRoot extends Command { * @throws \Exception */ protected function moveAllKeys($oldRoot, $newRoot, OutputInterface $output) { - $output->writeln("Start to move keys:"); + $output->writeln('Start to move keys:'); if ($this->rootView->is_dir($oldRoot) === false) { - $output->writeln("No old keys found: Nothing needs to be moved"); + $output->writeln('No old keys found: Nothing needs to be moved'); return false; } @@ -141,8 +123,8 @@ class ChangeKeyStorageRoot extends Command { */ protected function moveSystemKeys($oldRoot, $newRoot) { if ( - $this->rootView->is_dir($oldRoot . '/files_encryption') && - $this->targetExists($newRoot . '/files_encryption') === false + $this->rootView->is_dir($oldRoot . '/files_encryption') + && $this->targetExists($newRoot . '/files_encryption') === false ) { $this->rootView->rename($oldRoot . '/files_encryption', $newRoot . '/files_encryption'); } @@ -201,8 +183,8 @@ class ChangeKeyStorageRoot extends Command { $source = $oldRoot . '/' . $user . '/files_encryption'; $target = $newRoot . '/' . $user . '/files_encryption'; if ( - $this->rootView->is_dir($source) && - $this->targetExists($target) === false + $this->rootView->is_dir($source) + && $this->targetExists($target) === false ) { $this->prepareParentFolder($newRoot . '/' . $user); $this->rootView->rename($source, $target); diff --git a/core/Command/Encryption/DecryptAll.php b/core/Command/Encryption/DecryptAll.php index 137b12141f7..92e2ba787e2 100644 --- a/core/Command/Encryption/DecryptAll.php +++ b/core/Command/Encryption/DecryptAll.php @@ -1,32 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author davitol <dtoledo@solidgear.es> - * @author Evgeny Golyshev <eugulixes@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Marius Blüm <marius@lineone.io> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Ruben Homs <ruben@homs.codes> - * @author Sergio Bertolín <sbertolin@solidgear.es> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; @@ -104,10 +81,10 @@ class DecryptAll extends Command { $isMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); if ($isMaintenanceModeEnabled) { - $output->writeln("Maintenance mode must be disabled when starting decryption,"); - $output->writeln("in order to load the relevant encryption modules correctly."); - $output->writeln("Your instance will automatically be put to maintenance mode"); - $output->writeln("during the actual decryption of the files."); + $output->writeln('Maintenance mode must be disabled when starting decryption,'); + $output->writeln('in order to load the relevant encryption modules correctly.'); + $output->writeln('Your instance will automatically be put to maintenance mode'); + $output->writeln('during the actual decryption of the files.'); return 1; } diff --git a/core/Command/Encryption/Disable.php b/core/Command/Encryption/Disable.php index 4c6e3431f93..91d4ac82d23 100644 --- a/core/Command/Encryption/Disable.php +++ b/core/Command/Encryption/Disable.php @@ -1,23 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; diff --git a/core/Command/Encryption/Enable.php b/core/Command/Encryption/Enable.php index 2cbb315283e..2c476185692 100644 --- a/core/Command/Encryption/Enable.php +++ b/core/Command/Encryption/Enable.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; diff --git a/core/Command/Encryption/EncryptAll.php b/core/Command/Encryption/EncryptAll.php index cf4ee749791..f2c991471b6 100644 --- a/core/Command/Encryption/EncryptAll.php +++ b/core/Command/Encryption/EncryptAll.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Evgeny Golyshev <eugulixes@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Matthew Setter <matthew@matthewsetter.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; @@ -37,7 +18,6 @@ use Symfony\Component\Console\Question\ConfirmationQuestion; class EncryptAll extends Command { protected bool $wasTrashbinEnabled = false; - protected bool $wasMaintenanceModeEnabled = false; public function __construct( protected IManager $encryptionManager, @@ -53,7 +33,6 @@ class EncryptAll extends Command { */ protected function forceMaintenanceAndTrashbin(): void { $this->wasTrashbinEnabled = (bool)$this->appManager->isEnabledForUser('files_trashbin'); - $this->wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance'); $this->config->setSystemValue('maintenance', true); $this->appManager->disableApp('files_trashbin'); } @@ -62,7 +41,7 @@ class EncryptAll extends Command { * Reset the maintenance mode and re-enable the trashbin app */ protected function resetMaintenanceAndTrashbin(): void { - $this->config->setSystemValue('maintenance', $this->wasMaintenanceModeEnabled); + $this->config->setSystemValue('maintenance', false); if ($this->wasTrashbinEnabled) { $this->appManager->enableApp('files_trashbin'); } @@ -93,6 +72,11 @@ class EncryptAll extends Command { throw new \Exception('Server side encryption is not enabled'); } + if ($this->config->getSystemValueBool('maintenance')) { + $output->writeln('<error>This command cannot be run with maintenance mode enabled.</error>'); + return self::FAILURE; + } + $output->writeln("\n"); $output->writeln('You are about to encrypt all files stored in your Nextcloud installation.'); $output->writeln('Depending on the number of available files, and their size, this may take quite some time.'); @@ -112,9 +96,9 @@ class EncryptAll extends Command { } $this->resetMaintenanceAndTrashbin(); - return 0; + return self::SUCCESS; } $output->writeln('aborted'); - return 1; + return self::FAILURE; } } diff --git a/core/Command/Encryption/ListModules.php b/core/Command/Encryption/ListModules.php index 46be88864a7..bf02c29f432 100644 --- a/core/Command/Encryption/ListModules.php +++ b/core/Command/Encryption/ListModules.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Ruben Homs <ruben@homs.codes> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; @@ -49,8 +33,8 @@ class ListModules extends Base { protected function execute(InputInterface $input, OutputInterface $output): int { $isMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); if ($isMaintenanceModeEnabled) { - $output->writeln("Maintenance mode must be disabled when listing modules"); - $output->writeln("in order to list the relevant encryption modules correctly."); + $output->writeln('Maintenance mode must be disabled when listing modules'); + $output->writeln('in order to list the relevant encryption modules correctly.'); return 1; } @@ -73,7 +57,7 @@ class ListModules extends Base { */ protected function writeModuleList(InputInterface $input, OutputInterface $output, $items) { if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) { - array_walk($items, function (&$item) { + array_walk($items, function (&$item): void { if (!$item['default']) { $item = $item['displayName']; } else { diff --git a/core/Command/Encryption/MigrateKeyStorage.php b/core/Command/Encryption/MigrateKeyStorage.php index 2441aa9cc1a..937b17cde5f 100644 --- a/core/Command/Encryption/MigrateKeyStorage.php +++ b/core/Command/Encryption/MigrateKeyStorage.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Encryption; @@ -47,7 +30,7 @@ class MigrateKeyStorage extends Command { parent::__construct(); } - protected function configure() { + protected function configure(): void { parent::configure(); $this ->setName('encryption:migrate-key-storage-format') @@ -57,9 +40,9 @@ class MigrateKeyStorage extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $root = $this->util->getKeyStorageRoot(); - $output->writeln("Updating key storage format"); + $output->writeln('Updating key storage format'); $this->updateKeys($root, $output); - $output->writeln("Key storage format successfully updated"); + $output->writeln('Key storage format successfully updated'); return 0; } @@ -67,15 +50,12 @@ class MigrateKeyStorage extends Command { /** * Move keys to new key storage root * - * @param string $root - * @param OutputInterface $output - * @return bool * @throws \Exception */ protected function updateKeys(string $root, OutputInterface $output): bool { - $output->writeln("Start to update the keys:"); + $output->writeln('Start to update the keys:'); - $this->updateSystemKeys($root); + $this->updateSystemKeys($root, $output); $this->updateUsersKeys($root, $output); $this->config->deleteSystemValue('encryption.key_storage_migrated'); return true; @@ -84,15 +64,15 @@ class MigrateKeyStorage extends Command { /** * Move system key folder */ - protected function updateSystemKeys(string $root): void { + protected function updateSystemKeys(string $root, OutputInterface $output): void { if (!$this->rootView->is_dir($root . '/files_encryption')) { return; } - $this->traverseKeys($root . '/files_encryption', null); + $this->traverseKeys($root . '/files_encryption', null, $output); } - private function traverseKeys(string $folder, ?string $uid) { + private function traverseKeys(string $folder, ?string $uid, OutputInterface $output): void { $listing = $this->rootView->getDirectoryContent($folder); foreach ($listing as $node) { @@ -100,14 +80,19 @@ class MigrateKeyStorage extends Command { continue; } - if ($node['name'] === 'fileKey' || - str_ends_with($node['name'], '.privateKey') || - str_ends_with($node['name'], '.publicKey') || - str_ends_with($node['name'], '.shareKey')) { + if ($node['name'] === 'fileKey' + || str_ends_with($node['name'], '.privateKey') + || str_ends_with($node['name'], '.publicKey') + || str_ends_with($node['name'], '.shareKey')) { $path = $folder . '/' . $node['name']; $content = $this->rootView->file_get_contents($path); + if ($content === false) { + $output->writeln("<error>Failed to open path $path</error>"); + continue; + } + try { $this->crypto->decrypt($content); continue; @@ -126,14 +111,14 @@ class MigrateKeyStorage extends Command { } } - private function traverseFileKeys(string $folder) { + private function traverseFileKeys(string $folder, OutputInterface $output): void { $listing = $this->rootView->getDirectoryContent($folder); foreach ($listing as $node) { if ($node['mimetype'] === 'httpd/unix-directory') { - $this->traverseFileKeys($folder . '/' . $node['name']); + $this->traverseFileKeys($folder . '/' . $node['name'], $output); } else { - $endsWith = function ($haystack, $needle) { + $endsWith = function (string $haystack, string $needle): bool { $length = strlen($needle); if ($length === 0) { return true; @@ -142,14 +127,19 @@ class MigrateKeyStorage extends Command { return (substr($haystack, -$length) === $needle); }; - if ($node['name'] === 'fileKey' || - $endsWith($node['name'], '.privateKey') || - $endsWith($node['name'], '.publicKey') || - $endsWith($node['name'], '.shareKey')) { + if ($node['name'] === 'fileKey' + || $endsWith($node['name'], '.privateKey') + || $endsWith($node['name'], '.publicKey') + || $endsWith($node['name'], '.shareKey')) { $path = $folder . '/' . $node['name']; $content = $this->rootView->file_get_contents($path); + if ($content === false) { + $output->writeln("<error>Failed to open path $path</error>"); + continue; + } + try { $this->crypto->decrypt($content); continue; @@ -171,10 +161,8 @@ class MigrateKeyStorage extends Command { /** * setup file system for the given user - * - * @param string $uid */ - protected function setupUserFS($uid) { + protected function setupUserFS(string $uid): void { \OC_Util::tearDownFS(); \OC_Util::setupFS($uid); } @@ -182,11 +170,8 @@ class MigrateKeyStorage extends Command { /** * iterate over each user and move the keys to the new storage - * - * @param string $root - * @param OutputInterface $output */ - protected function updateUsersKeys(string $root, OutputInterface $output) { + protected function updateUsersKeys(string $root, OutputInterface $output): void { $progress = new ProgressBar($output); $progress->start(); @@ -198,7 +183,7 @@ class MigrateKeyStorage extends Command { foreach ($users as $user) { $progress->advance(); $this->setupUserFS($user); - $this->updateUserKeys($root, $user); + $this->updateUserKeys($root, $user, $output); } $offset += $limit; } while (count($users) >= $limit); @@ -209,20 +194,18 @@ class MigrateKeyStorage extends Command { /** * move user encryption folder to new root folder * - * @param string $root - * @param string $user * @throws \Exception */ - protected function updateUserKeys(string $root, string $user) { + protected function updateUserKeys(string $root, string $user, OutputInterface $output): void { if ($this->userManager->userExists($user)) { $source = $root . '/' . $user . '/files_encryption/OC_DEFAULT_MODULE'; if ($this->rootView->is_dir($source)) { - $this->traverseKeys($source, $user); + $this->traverseKeys($source, $user, $output); } $source = $root . '/' . $user . '/files_encryption/keys'; if ($this->rootView->is_dir($source)) { - $this->traverseFileKeys($source); + $this->traverseFileKeys($source, $output); } } } diff --git a/core/Command/Encryption/SetDefaultModule.php b/core/Command/Encryption/SetDefaultModule.php index f4106926778..d10872afd38 100644 --- a/core/Command/Encryption/SetDefaultModule.php +++ b/core/Command/Encryption/SetDefaultModule.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Ruben Homs <ruben@homs.codes> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; @@ -55,8 +39,8 @@ class SetDefaultModule extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $isMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); if ($isMaintenanceModeEnabled) { - $output->writeln("Maintenance mode must be disabled when setting default module,"); - $output->writeln("in order to load the relevant encryption modules correctly."); + $output->writeln('Maintenance mode must be disabled when setting default module,'); + $output->writeln('in order to load the relevant encryption modules correctly.'); return 1; } diff --git a/core/Command/Encryption/ShowKeyStorageRoot.php b/core/Command/Encryption/ShowKeyStorageRoot.php index 71b396540fd..8cf97076249 100644 --- a/core/Command/Encryption/ShowKeyStorageRoot.php +++ b/core/Command/Encryption/ShowKeyStorageRoot.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; diff --git a/core/Command/Encryption/Status.php b/core/Command/Encryption/Status.php index 691b399203d..6e4f7d16d0c 100644 --- a/core/Command/Encryption/Status.php +++ b/core/Command/Encryption/Status.php @@ -1,23 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Encryption; diff --git a/core/Command/FilesMetadata/Get.php b/core/Command/FilesMetadata/Get.php index 99bc167f71d..0b022d0951b 100644 --- a/core/Command/FilesMetadata/Get.php +++ b/core/Command/FilesMetadata/Get.php @@ -3,25 +3,8 @@ 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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\FilesMetadata; @@ -48,35 +31,35 @@ class Get extends Command { 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' - ); + ->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' + ); } /** @@ -96,12 +79,12 @@ class Get extends Command { } if ($input->getOption('refresh')) { - $node = $this->rootFolder->getUserFolder($input->getArgument('userId'))->getById($fileId); - if (count($node) === 0) { + $node = $this->rootFolder->getUserFolder($input->getArgument('userId'))->getFirstNodeById($fileId); + if (!$node) { throw new NotFoundException(); } $metadata = $this->filesMetadataManager->refreshMetadata( - $node[0], + $node, IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND ); } else { diff --git a/core/Command/Group/Add.php b/core/Command/Group/Add.php index 40502762e95..26d44c7ea83 100644 --- a/core/Command/Group/Add.php +++ b/core/Command/Group/Add.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Denis Mosolov <denismosolov@gmail.com> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Denis Mosolov <denismosolov@gmail.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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Group; diff --git a/core/Command/Group/AddUser.php b/core/Command/Group/AddUser.php index a66d2898ef9..999113390af 100644 --- a/core/Command/Group/AddUser.php +++ b/core/Command/Group/AddUser.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Group; diff --git a/core/Command/Group/Delete.php b/core/Command/Group/Delete.php index c7cbf0aa0f6..a2736476920 100644 --- a/core/Command/Group/Delete.php +++ b/core/Command/Group/Delete.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Denis Mosolov <denismosolov@gmail.com> - * - * @author Denis Mosolov <denismosolov@gmail.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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Group; diff --git a/core/Command/Group/Info.php b/core/Command/Group/Info.php index 1dab56e1a89..d42d2a64577 100644 --- a/core/Command/Group/Info.php +++ b/core/Command/Group/Info.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021, hosting.de, Johannes Leuker <developers@hosting.de> - * - * @author Johannes Leuker <j.leuker@hosting.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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Group; diff --git a/core/Command/Group/ListCommand.php b/core/Command/Group/ListCommand.php index 22ce4cb0317..01522a23f7f 100644 --- a/core/Command/Group/ListCommand.php +++ b/core/Command/Group/ListCommand.php @@ -1,32 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Leuker <j.leuker@hosting.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Group; use OC\Core\Command\Base; use OCP\IGroup; use OCP\IGroupManager; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -42,6 +25,12 @@ class ListCommand extends Base { $this ->setName('group:list') ->setDescription('list configured groups') + ->addArgument( + 'searchstring', + InputArgument::OPTIONAL, + 'Filter the groups to only those matching the search string', + '' + ) ->addOption( 'limit', 'l', @@ -69,7 +58,7 @@ class ListCommand extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $groups = $this->groupManager->search('', (int)$input->getOption('limit'), (int)$input->getOption('offset')); + $groups = $this->groupManager->search((string)$input->getArgument('searchstring'), (int)$input->getOption('limit'), (int)$input->getOption('offset')); $this->writeArrayInOutputFormat($input, $output, $this->formatGroups($groups, (bool)$input->getOption('info'))); return 0; } @@ -87,25 +76,19 @@ class ListCommand extends Base { /** * @param IGroup[] $groups - * @return array */ - private function formatGroups(array $groups, bool $addInfo = false) { - $keys = array_map(function (IGroup $group) { - return $group->getGID(); - }, $groups); - - if ($addInfo) { - $values = array_map(function (IGroup $group) { - return [ + private function formatGroups(array $groups, bool $addInfo = false): \Generator { + foreach ($groups as $group) { + if ($addInfo) { + $value = [ + 'displayName' => $group->getDisplayName(), 'backends' => $group->getBackendNames(), 'users' => $this->usersForGroup($group), ]; - }, $groups); - } else { - $values = array_map(function (IGroup $group) { - return $this->usersForGroup($group); - }, $groups); + } else { + $value = $this->usersForGroup($group); + } + yield $group->getGID() => $value; } - return array_combine($keys, $values); } } diff --git a/core/Command/Group/RemoveUser.php b/core/Command/Group/RemoveUser.php index 6c7d4ce4d84..952fc6e7712 100644 --- a/core/Command/Group/RemoveUser.php +++ b/core/Command/Group/RemoveUser.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Group; diff --git a/core/Command/Info/File.php b/core/Command/Info/File.php index d62feeb5a17..287bd0e29cb 100644 --- a/core/Command/Info/File.php +++ b/core/Command/Info/File.php @@ -1,12 +1,19 @@ <?php declare(strict_types=1); - +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ namespace OC\Core\Command\Info; use OC\Files\ObjectStore\ObjectStoreStorage; +use OC\Files\Storage\Wrapper\Encryption; +use OC\Files\Storage\Wrapper\Wrapper; +use OC\Files\View; use OCA\Files_External\Config\ExternalMountPoint; use OCA\GroupFolders\Mount\GroupMountPoint; +use OCP\Files\File as OCPFile; use OCP\Files\Folder; use OCP\Files\IHomeStorage; use OCP\Files\Mount\IMountPoint; @@ -23,21 +30,25 @@ 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"); + $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"); + ->addArgument('file', InputArgument::REQUIRED, 'File id or path') + ->addOption('children', 'c', InputOption::VALUE_NONE, 'List children of folders') + ->addOption('storage-tree', null, InputOption::VALUE_NONE, 'Show storage and cache wrapping tree'); } public function execute(InputInterface $input, OutputInterface $output): int { @@ -50,42 +61,66 @@ class File extends Command { } $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")); - $output->writeln(" size: " . Util::humanFileSize($node->getSize())); - $output->writeln(" etag: " . $node->getEtag()); + $output->writeln(' fileid: ' . $node->getId()); + $output->writeln(' mimetype: ' . $node->getMimetype()); + $output->writeln(' modified: ' . (string)$this->l10n->l('datetime', $node->getMTime())); + + if ($node instanceof OCPFile && $node->isEncrypted()) { + $output->writeln(' ' . 'server-side encrypted: yes'); + $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); + } + $storage = $node->getStorage(); + if ($storage->instanceOfStorage(Encryption::class)) { + /** @var Encryption $storage */ + if (!$storage->hasValidHeader($node->getInternalPath())) { + $output->writeln(' <error>file doesn\'t have a valid encryption header</error>'); + } + } else { + $output->writeln(' <error>file is marked as encrypted, but encryption doesn\'t seem to be setup</error>'); + } + } + + if ($node instanceof Folder && $node->isEncrypted() || $node instanceof OCPFile && $node->getParent()->isEncrypted()) { + $output->writeln(' ' . 'end-to-end encrypted: yes'); + } + + $output->writeln(' size: ' . Util::humanFileSize($node->getSize())); + $output->writeln(' etag: ' . $node->getEtag()); + $output->writeln(' permissions: ' . $this->fileUtils->formatPermissions($node->getType(), $node->getPermissions())); 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."); + $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) . ":"); + $output->writeln(' children: ' . count($children) . ':'); foreach ($children as $child) { - $output->writeln(" - " . $child->getName()); + $output->writeln(' - ' . $child->getName()); } } else { - $output->writeln(" children: " . count($children) . " (use <info>--children</info> option to list)"); + $output->writeln(' children: ' . count($children) . ' (use <info>--children</info> option to list)'); } } - $this->outputStorageDetails($node->getMountPoint(), $node, $output); + $this->outputStorageDetails($node->getMountPoint(), $node, $input, $output); $filesPerUser = $this->fileUtils->getFilesByUser($node); - $output->writeln(""); - $output->writeln("The following users have access to the file"); - $output->writeln(""); + $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())); + $output->writeln(' ' . $userFile->getPath() . ': ' . $this->fileUtils->formatPermissions($userFile->getType(), $userFile->getPermissions())); $mount = $userFile->getMountPoint(); - $output->writeln(" " . $this->fileUtils->formatMountType($mount)); + $output->writeln(' ' . $this->fileUtils->formatMountType($mount)); } } @@ -96,13 +131,13 @@ class File extends Command { * @psalm-suppress UndefinedClass * @psalm-suppress UndefinedInterfaceMethod */ - private function outputStorageDetails(IMountPoint $mountPoint, Node $node, OutputInterface $output): void { + private function outputStorageDetails(IMountPoint $mountPoint, Node $node, InputInterface $input, OutputInterface $output): void { $storage = $mountPoint->getStorage(); if (!$storage) { return; } if (!$storage->instanceOfStorage(IHomeStorage::class)) { - $output->writeln(" mounted at: " . $mountPoint->getMountPoint()); + $output->writeln(' mounted at: ' . $mountPoint->getMountPoint()); } if ($storage->instanceOfStorage(ObjectStoreStorage::class)) { /** @var ObjectStoreStorage $storage */ @@ -110,9 +145,9 @@ class File extends Command { $parts = explode(':', $objectStoreId); /** @var string $bucket */ $bucket = array_pop($parts); - $output->writeln(" bucket: " . $bucket); + $output->writeln(' bucket: ' . $bucket); if ($node instanceof \OC\Files\Node\File) { - $output->writeln(" object id: " . $storage->getURN($node->getId())); + $output->writeln(' object id: ' . $storage->getURN($node->getId())); try { $fh = $node->fopen('r'); if (!$fh) { @@ -121,23 +156,33 @@ class File extends Command { $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"); + $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>"); + $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>"); + $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()); + $output->writeln(' external storage id: ' . $storageConfig->getId()); + $output->writeln(' external type: ' . $storageConfig->getBackend()->getText()); } elseif ($mountPoint instanceof GroupMountPoint) { - $output->writeln(" groupfolder id: " . $mountPoint->getFolderId()); + $output->writeln(' groupfolder id: ' . $mountPoint->getFolderId()); } + if ($input->getOption('storage-tree')) { + $storageTmp = $storage; + $storageClass = get_class($storageTmp) . ' (cache:' . get_class($storageTmp->getCache()) . ')'; + while ($storageTmp instanceof Wrapper) { + $storageTmp = $storageTmp->getWrapperStorage(); + $storageClass .= "\n\t" . '> ' . get_class($storageTmp) . ' (cache:' . get_class($storageTmp->getCache()) . ')'; + } + $output->writeln(' storage wrapping: ' . $storageClass); + } + } } diff --git a/core/Command/Info/FileUtils.php b/core/Command/Info/FileUtils.php index 694f30f78b4..bc07535a289 100644 --- a/core/Command/Info/FileUtils.php +++ b/core/Command/Info/FileUtils.php @@ -2,32 +2,19 @@ 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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Info; +use OC\User\NoUserException; 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\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Config\IUserMountCache; use OCP\Files\FileInfo; use OCP\Files\Folder; @@ -36,22 +23,28 @@ use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; use OCP\Files\Node; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IDBConnection; use OCP\Share\IShare; use OCP\Util; use Symfony\Component\Console\Output\OutputInterface; +/** + * @psalm-type StorageInfo array{numeric_id: int, id: string, available: bool, last_checked: ?\DateTime, files: int, mount_id: ?int} + */ class FileUtils { public function __construct( private IRootFolder $rootFolder, private IUserMountCache $userMountCache, + private IDBConnection $connection, ) { } /** * @param FileInfo $file * @return array<string, Node[]> - * @throws \OCP\Files\NotPermittedException - * @throws \OC\User\NoUserException + * @throws NotPermittedException + * @throws NoUserException */ public function getFilesByUser(FileInfo $file): array { $id = $file->getId(); @@ -61,13 +54,12 @@ class FileUtils { $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); + foreach ($mounts as $cachedMount) { + $mount = $this->rootFolder->getMount($cachedMount->getMountPoint()); + $cache = $mount->getStorage()->getCache(); + $cacheEntry = $cache->get($id); + $node = $this->rootFolder->getNodeFromCacheEntryAndMount($cacheEntry, $mount); + $result[$cachedMount->getUser()->getUID()][] = $node; } return $result; @@ -85,13 +77,9 @@ class FileUtils { if (!$mounts) { return null; } - $mount = $mounts[0]; + $mount = reset($mounts); $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); - $nodes = $userFolder->getById((int)$fileInput); - if (!$nodes) { - return null; - } - return $nodes[0]; + return $userFolder->getFirstNodeById((int)$fileInput); } else { try { return $this->rootFolder->get($fileInput); @@ -103,18 +91,18 @@ class FileUtils { 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"; + 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"]; + $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); + return implode(', ', $perms); } /** @@ -124,29 +112,29 @@ class FileUtils { public function formatMountType(IMountPoint $mountPoint): string { $storage = $mountPoint->getStorage(); if ($storage && $storage->instanceOfStorage(IHomeStorage::class)) { - return "home storage"; + 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() . ")"; + return $share->getSharedBy() . ' (via ' . $shareType . ' ' . $share->getSharedWith() . ')'; } else { return $share->getSharedBy(); } }, $shares); - $description = "shared by " . implode(', ', $sharedBy); + $description = 'shared by ' . implode(', ', $sharedBy); if ($share->getSharedBy() !== $share->getShareOwner()) { - $description .= " owned by " . $share->getShareOwner(); + $description .= ' owned by ' . $share->getShareOwner(); } return $description; } elseif ($mountPoint instanceof GroupMountPoint) { - return "groupfolder " . $mountPoint->getFolderId(); + return 'groupfolder ' . $mountPoint->getFolderId(); } elseif ($mountPoint instanceof ExternalMountPoint) { - return "external storage " . $mountPoint->getStorageConfig()->getId(); + return 'external storage ' . $mountPoint->getStorageConfig()->getId(); } elseif ($mountPoint instanceof CircleMount) { - return "circle"; + return 'circle'; } return get_class($mountPoint); } @@ -154,17 +142,17 @@ class FileUtils { public function formatShareType(IShare $share): ?string { switch ($share->getShareType()) { case IShare::TYPE_GROUP: - return "group"; + return 'group'; case IShare::TYPE_CIRCLE: - return "circle"; + return 'circle'; case IShare::TYPE_DECK: - return "deck"; + return 'deck'; case IShare::TYPE_ROOM: - return "room"; + return 'room'; case IShare::TYPE_USER: return null; default: - return "Unknown (" . $share->getShareType() . ")"; + return 'Unknown (' . $share->getShareType() . ')'; } } @@ -216,7 +204,7 @@ class FileUtils { $count += 1; /** @var Node $child */ - $output->writeln("$prefix- " . $child->getName() . ": <info>" . Util::humanFileSize($child->getSize()) . "</info>"); + $output->writeln("$prefix- " . $child->getName() . ': <info>' . Util::humanFileSize($child->getSize()) . '</info>'); if ($child instanceof Folder) { $recurseSizeLimits = $sizeLimits; if (!$all) { @@ -231,11 +219,107 @@ class FileUtils { } sort($recurseSizeLimits); } - $recurseCount = $this->outputLargeFilesTree($output, $child, $prefix . " ", $recurseSizeLimits, $all); + $recurseCount = $this->outputLargeFilesTree($output, $child, $prefix . ' ', $recurseSizeLimits, $all); $sizeLimits = array_slice($sizeLimits, $recurseCount); $count += $recurseCount; } } return $count; } + + public function getNumericStorageId(string $id): ?int { + if (is_numeric($id)) { + return (int)$id; + } + $query = $this->connection->getQueryBuilder(); + $query->select('numeric_id') + ->from('storages') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $result = $query->executeQuery()->fetchOne(); + return $result ? (int)$result : null; + } + + /** + * @param int|null $limit + * @return ?StorageInfo + * @throws \OCP\DB\Exception + */ + public function getStorage(int $id): ?array { + $query = $this->connection->getQueryBuilder(); + $query->select('numeric_id', 's.id', 'available', 'last_checked', 'mount_id') + ->selectAlias($query->func()->count('fileid'), 'files') + ->from('storages', 's') + ->innerJoin('s', 'filecache', 'f', $query->expr()->eq('f.storage', 's.numeric_id')) + ->leftJoin('s', 'mounts', 'm', $query->expr()->eq('s.numeric_id', 'm.storage_id')) + ->where($query->expr()->eq('s.numeric_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))) + ->groupBy('s.numeric_id', 's.id', 's.available', 's.last_checked', 'mount_id'); + $row = $query->executeQuery()->fetch(); + if ($row) { + return [ + 'numeric_id' => $row['numeric_id'], + 'id' => $row['id'], + 'files' => $row['files'], + 'available' => (bool)$row['available'], + 'last_checked' => $row['last_checked'] ? new \DateTime('@' . $row['last_checked']) : null, + 'mount_id' => $row['mount_id'], + ]; + } else { + return null; + } + } + + /** + * @param int|null $limit + * @return \Iterator<StorageInfo> + * @throws \OCP\DB\Exception + */ + public function listStorages(?int $limit): \Iterator { + $query = $this->connection->getQueryBuilder(); + $query->select('numeric_id', 's.id', 'available', 'last_checked', 'mount_id') + ->selectAlias($query->func()->count('fileid'), 'files') + ->from('storages', 's') + ->innerJoin('s', 'filecache', 'f', $query->expr()->eq('f.storage', 's.numeric_id')) + ->leftJoin('s', 'mounts', 'm', $query->expr()->eq('s.numeric_id', 'm.storage_id')) + ->groupBy('s.numeric_id', 's.id', 's.available', 's.last_checked', 'mount_id') + ->orderBy('files', 'DESC'); + if ($limit !== null) { + $query->setMaxResults($limit); + } + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + yield [ + 'numeric_id' => $row['numeric_id'], + 'id' => $row['id'], + 'files' => $row['files'], + 'available' => (bool)$row['available'], + 'last_checked' => $row['last_checked'] ? new \DateTime('@' . $row['last_checked']) : null, + 'mount_id' => $row['mount_id'], + ]; + } + } + + /** + * @param StorageInfo $storage + * @return array + */ + public function formatStorage(array $storage): array { + return [ + 'numeric_id' => $storage['numeric_id'], + 'id' => $storage['id'], + 'files' => $storage['files'], + 'available' => $storage['available'] ? 'true' : 'false', + 'last_checked' => $storage['last_checked']?->format(\DATE_ATOM), + 'external_mount_id' => $storage['mount_id'], + ]; + } + + /** + * @param \Iterator<StorageInfo> $storages + * @return \Iterator + */ + public function formatStorages(\Iterator $storages): \Iterator { + foreach ($storages as $storage) { + yield $this->formatStorage($storage); + } + } } diff --git a/core/Command/Info/Space.php b/core/Command/Info/Space.php index dfbfcf848c1..35c1d5c3228 100644 --- a/core/Command/Info/Space.php +++ b/core/Command/Info/Space.php @@ -2,23 +2,8 @@ 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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Info; @@ -42,9 +27,9 @@ class Space extends Command { $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"); + ->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 { @@ -56,7 +41,7 @@ class Space extends Command { $output->writeln("<error>file $fileInput not found</error>"); return 1; } - $output->writeln($node->getName() . ": <info>" . Util::humanFileSize($node->getSize()) . "</info>"); + $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); diff --git a/core/Command/Info/Storage.php b/core/Command/Info/Storage.php new file mode 100644 index 00000000000..c1d0e1725ca --- /dev/null +++ b/core/Command/Info/Storage.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Info; + +use OC\Core\Command\Base; +use OCP\IDBConnection; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Storage extends Base { + public function __construct( + private readonly IDBConnection $connection, + private readonly FileUtils $fileUtils, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('info:storage') + ->setDescription('Get information a single storage') + ->addArgument('storage', InputArgument::REQUIRED, 'Storage to get information for'); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $storage = $input->getArgument('storage'); + $storageId = $this->fileUtils->getNumericStorageId($storage); + if (!$storageId) { + $output->writeln('<error>No storage with id ' . $storage . ' found</error>'); + return 1; + } + + $info = $this->fileUtils->getStorage($storageId); + if (!$info) { + $output->writeln('<error>No storage with id ' . $storage . ' found</error>'); + return 1; + } + $this->writeArrayInOutputFormat($input, $output, $this->fileUtils->formatStorage($info)); + return 0; + } +} diff --git a/core/Command/Info/Storages.php b/core/Command/Info/Storages.php new file mode 100644 index 00000000000..ff767a2ff5d --- /dev/null +++ b/core/Command/Info/Storages.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Info; + +use OC\Core\Command\Base; +use OCP\IDBConnection; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Storages extends Base { + public function __construct( + private readonly IDBConnection $connection, + private readonly FileUtils $fileUtils, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('info:storages') + ->setDescription('List storages ordered by the number of files') + ->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'Number of storages to display', 25) + ->addOption('all', 'a', InputOption::VALUE_NONE, 'Display all storages'); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $count = (int)$input->getOption('count'); + $all = $input->getOption('all'); + + $limit = $all ? null : $count; + $storages = $this->fileUtils->listStorages($limit); + $this->writeStreamingTableInOutputFormat($input, $output, $this->fileUtils->formatStorages($storages), 100); + return 0; + } +} diff --git a/core/Command/Integrity/CheckApp.php b/core/Command/Integrity/CheckApp.php index fe999fed9fd..0145a3f8070 100644 --- a/core/Command/Integrity/CheckApp.php +++ b/core/Command/Integrity/CheckApp.php @@ -1,34 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Carla Schroder <carla@owncloud.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Integrity; use OC\Core\Command\Base; use OC\IntegrityCheck\Checker; +use OC\IntegrityCheck\Helpers\AppLocator; +use OC\IntegrityCheck\Helpers\FileAccessHelper; +use OCP\App\IAppManager; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -42,6 +25,9 @@ use Symfony\Component\Console\Output\OutputInterface; class CheckApp extends Base { public function __construct( private Checker $checker, + private AppLocator $appLocator, + private FileAccessHelper $fileAccessHelper, + private IAppManager $appManager, ) { parent::__construct(); } @@ -54,23 +40,58 @@ class CheckApp extends Base { $this ->setName('integrity:check-app') ->setDescription('Check integrity of an app using a signature.') - ->addArgument('appid', InputArgument::REQUIRED, 'Application to check') - ->addOption('path', null, InputOption::VALUE_OPTIONAL, 'Path to application. If none is given it will be guessed.'); + ->addArgument('appid', InputArgument::OPTIONAL, 'Application to check') + ->addOption('path', null, InputOption::VALUE_OPTIONAL, 'Path to application. If none is given it will be guessed.') + ->addOption('all', null, InputOption::VALUE_NONE, 'Check integrity of all apps.'); } /** * {@inheritdoc } */ protected function execute(InputInterface $input, OutputInterface $output): int { - $appid = $input->getArgument('appid'); - $path = (string)$input->getOption('path'); - $result = $this->checker->verifyAppSignature($appid, $path, true); - $this->writeArrayInOutputFormat($input, $output, $result); - if (count($result) > 0) { - $output->writeln('<error>' . count($result) . ' errors found</error>', OutputInterface::VERBOSITY_VERBOSE); + if ($input->getOption('all') && $input->getArgument('appid')) { + $output->writeln('<error>Option "--all" cannot be combined with an appid</error>'); + return 1; + } + + if (!$input->getArgument('appid') && !$input->getOption('all')) { + $output->writeln('<error>Please specify an appid, or "--all" to verify all apps</error>'); return 1; } - $output->writeln('<info>No errors found</info>', OutputInterface::VERBOSITY_VERBOSE); - return 0; + + if ($input->getArgument('appid')) { + $appIds = [$input->getArgument('appid')]; + } else { + $appIds = $this->appManager->getAllAppsInAppsFolders(); + } + + $errorsFound = false; + + foreach ($appIds as $appId) { + $path = (string)$input->getOption('path'); + if ($path === '') { + $path = $this->appLocator->getAppPath($appId); + } + + if ($this->appManager->isShipped($appId) || $this->fileAccessHelper->file_exists($path . '/appinfo/signature.json')) { + // Only verify if the application explicitly ships a signature.json file + $result = $this->checker->verifyAppSignature($appId, $path, true); + + if (count($result) > 0) { + $output->writeln('<error>' . $appId . ': ' . count($result) . ' errors found:</error>'); + $this->writeArrayInOutputFormat($input, $output, $result); + $errorsFound = true; + } + } else { + $output->writeln('<comment>' . $appId . ': ' . 'App signature not found, skipping app integrity check</comment>'); + } + } + + if (!$errorsFound) { + $output->writeln('<info>No errors found</info>', OutputInterface::VERBOSITY_VERBOSE); + return 0; + } + + return 1; } } diff --git a/core/Command/Integrity/CheckCore.php b/core/Command/Integrity/CheckCore.php index 0bcb75fa6be..49086e94d26 100644 --- a/core/Command/Integrity/CheckCore.php +++ b/core/Command/Integrity/CheckCore.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Carla Schroder <carla@owncloud.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Integrity; diff --git a/core/Command/Integrity/SignApp.php b/core/Command/Integrity/SignApp.php index ebdc714c8c2..d307bc58985 100644 --- a/core/Command/Integrity/SignApp.php +++ b/core/Command/Integrity/SignApp.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Integrity; @@ -68,7 +51,7 @@ class SignApp extends Command { $documentationUrl = $this->urlGenerator->linkToDocs('developer-code-integrity'); $output->writeln('This command requires the --path, --privateKey and --certificate.'); $output->writeln('Example: ./occ integrity:sign-app --path="/Users/lukasreschke/Programming/myapp/" --privateKey="/Users/lukasreschke/private/myapp.key" --certificate="/Users/lukasreschke/public/mycert.crt"'); - $output->writeln('For more information please consult the documentation: '. $documentationUrl); + $output->writeln('For more information please consult the documentation: ' . $documentationUrl); return 1; } @@ -92,7 +75,7 @@ class SignApp extends Command { $x509->setPrivateKey($rsa); try { $this->checker->writeAppSignature($path, $x509, $rsa); - $output->writeln('Successfully signed "'.$path.'"'); + $output->writeln('Successfully signed "' . $path . '"'); } catch (\Exception $e) { $output->writeln('Error: ' . $e->getMessage()); return 1; diff --git a/core/Command/Integrity/SignCore.php b/core/Command/Integrity/SignCore.php index 48cfcadd10d..ed80091ec38 100644 --- a/core/Command/Integrity/SignCore.php +++ b/core/Command/Integrity/SignCore.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Integrity; diff --git a/core/Command/InterruptedException.php b/core/Command/InterruptedException.php index 2e2b5b979fc..661e6672577 100644 --- a/core/Command/InterruptedException.php +++ b/core/Command/InterruptedException.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2017, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2017 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command; diff --git a/core/Command/L10n/CreateJs.php b/core/Command/L10n/CreateJs.php index 6472e7aec1c..64a21e6d48c 100644 --- a/core/Command/L10n/CreateJs.php +++ b/core/Command/L10n/CreateJs.php @@ -1,31 +1,18 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\L10n; use DirectoryIterator; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; use Symfony\Component\Console\Command\Command; @@ -35,6 +22,12 @@ use Symfony\Component\Console\Output\OutputInterface; use UnexpectedValueException; class CreateJs extends Command implements CompletionAwareInterface { + public function __construct( + protected IAppManager $appManager, + ) { + parent::__construct(); + } + protected function configure() { $this ->setName('l10n:createjs') @@ -55,11 +48,7 @@ class CreateJs extends Command implements CompletionAwareInterface { $app = $input->getArgument('app'); $lang = $input->getArgument('lang'); - $path = \OC_App::getAppPath($app); - if ($path === false) { - $output->writeln("The app <$app> is unknown."); - return 1; - } + $path = $this->appManager->getAppPath($app); $languages = $lang; if (empty($lang)) { $languages = $this->getAllLanguages($path); @@ -159,10 +148,14 @@ class CreateJs extends Command implements CompletionAwareInterface { */ public function completeArgumentValues($argumentName, CompletionContext $context) { if ($argumentName === 'app') { - return \OC_App::getAllApps(); + return $this->appManager->getAllAppsInAppsFolders(); } elseif ($argumentName === 'lang') { $appName = $context->getWordAtIndex($context->getWordIndex() - 1); - return $this->getAllLanguages(\OC_App::getAppPath($appName)); + try { + return $this->getAllLanguages($this->appManager->getAppPath($appName)); + } catch (AppPathNotFoundException) { + return []; + } } return []; } diff --git a/core/Command/Log/File.php b/core/Command/Log/File.php index 978115d5aeb..ba5dad956e9 100644 --- a/core/Command/Log/File.php +++ b/core/Command/Log/File.php @@ -1,31 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Pulzer <t.pulzer@kniel.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Log; use OCP\IConfig; +use OCP\Util; use Stecman\Component\Symfony\Console\BashCompletion\Completion; use Stecman\Component\Symfony\Console\BashCompletion\Completion\ShellPathCompletion; @@ -79,7 +62,7 @@ class File extends Command implements Completion\CompletionAwareInterface { } if (($rotateSize = $input->getOption('rotate-size')) !== null) { - $rotateSize = \OCP\Util::computerFileSize($rotateSize); + $rotateSize = Util::computerFileSize($rotateSize); $this->validateRotateSize($rotateSize); $toBeSet['log_rotate_size'] = $rotateSize; } @@ -97,19 +80,19 @@ class File extends Command implements Completion\CompletionAwareInterface { } else { $enabledText = 'disabled'; } - $output->writeln('Log backend file: '.$enabledText); + $output->writeln('Log backend file: ' . $enabledText); - $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data'); - $defaultLogFile = rtrim($dataDir, '/').'/nextcloud.log'; - $output->writeln('Log file: '.$this->config->getSystemValue('logfile', $defaultLogFile)); + $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data'); + $defaultLogFile = rtrim($dataDir, '/') . '/nextcloud.log'; + $output->writeln('Log file: ' . $this->config->getSystemValue('logfile', $defaultLogFile)); $rotateSize = $this->config->getSystemValue('log_rotate_size', 100 * 1024 * 1024); if ($rotateSize) { - $rotateString = \OCP\Util::humanFileSize($rotateSize); + $rotateString = Util::humanFileSize($rotateSize); } else { $rotateString = 'disabled'; } - $output->writeln('Rotate at: '.$rotateString); + $output->writeln('Rotate at: ' . $rotateString); return 0; } diff --git a/core/Command/Log/Manage.php b/core/Command/Log/Manage.php index 7c25fdf8a6b..f67f0d969f6 100644 --- a/core/Command/Log/Manage.php +++ b/core/Command/Log/Manage.php @@ -1,28 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Ernst <jernst@indiecomputing.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tim Terhorst <mynamewastaken+gitlab@gmail.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Log; @@ -103,14 +84,14 @@ class Manage extends Command implements CompletionAwareInterface { // display configuration $backend = $this->config->getSystemValue('log_type', self::DEFAULT_BACKEND); - $output->writeln('Enabled logging backend: '.$backend); + $output->writeln('Enabled logging backend: ' . $backend); $levelNum = $this->config->getSystemValue('loglevel', self::DEFAULT_LOG_LEVEL); $level = $this->convertLevelNumber($levelNum); - $output->writeln('Log level: '.$level.' ('.$levelNum.')'); + $output->writeln('Log level: ' . $level . ' (' . $levelNum . ')'); $timezone = $this->config->getSystemValue('logtimezone', self::DEFAULT_TIMEZONE); - $output->writeln('Log timezone: '.$timezone); + $output->writeln('Log timezone: ' . $timezone); return 0; } @@ -119,7 +100,7 @@ class Manage extends Command implements CompletionAwareInterface { * @throws \InvalidArgumentException */ protected function validateBackend($backend) { - if (!class_exists('OC\\Log\\'.ucfirst($backend))) { + if (!class_exists('OC\\Log\\' . ucfirst($backend))) { throw new \InvalidArgumentException('Invalid backend'); } } diff --git a/core/Command/Maintenance/DataFingerprint.php b/core/Command/Maintenance/DataFingerprint.php index 3c15b95bc05..014d6c411a4 100644 --- a/core/Command/Maintenance/DataFingerprint.php +++ b/core/Command/Maintenance/DataFingerprint.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance; @@ -43,7 +28,9 @@ class DataFingerprint extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - $this->config->setSystemValue('data-fingerprint', md5($this->timeFactory->getTime())); + $fingerPrint = md5($this->timeFactory->getTime()); + $this->config->setSystemValue('data-fingerprint', $fingerPrint); + $output->writeln('<info>Updated data-fingerprint to ' . $fingerPrint . '</info>'); return 0; } } diff --git a/core/Command/Maintenance/Install.php b/core/Command/Maintenance/Install.php index f5d53c96234..6170c5a2638 100644 --- a/core/Command/Maintenance/Install.php +++ b/core/Command/Maintenance/Install.php @@ -1,44 +1,21 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Hansson <daniel@techandme.se> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Pulzer <t.pulzer@kniel.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance; use bantu\IniGetWrapper\IniGetWrapper; use InvalidArgumentException; use OC\Console\TimestampFormatter; -use OC\Installer; use OC\Migration\ConsoleOutput; use OC\Setup; use OC\SystemConfig; -use OCP\Defaults; -use Psr\Log\LoggerInterface; +use OCP\Server; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -56,7 +33,7 @@ class Install extends Command { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('maintenance:install') ->setDescription('install Nextcloud') @@ -64,35 +41,27 @@ 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('disable-admin-user', null, InputOption::VALUE_NONE, 'Disable the creation of an admin user') + ->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"); + ->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 = Server::get(Setup::class); $sysInfo = $setupHelper->getSystemInfo(true); $errors = $sysInfo['errors']; if (count($errors) > 0) { $this->printErrors($output, $errors); // ignore the OS X setup warning - if (count($errors) !== 1 || - (string)$errors[0]['error'] !== 'Mac OS X is not supported and Nextcloud will not work properly on this platform. Use it at your own risk! ') { + if (count($errors) !== 1 + || (string)$errors[0]['error'] !== 'Mac OS X is not supported and Nextcloud will not work properly on this platform. Use it at your own risk!') { return 1; } } @@ -118,7 +87,7 @@ class Install extends Command { 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"); + $output->writeln('Nextcloud was successfully installed'); return 0; } @@ -132,7 +101,7 @@ class Install extends Command { $db = strtolower($input->getOption('database')); if (!in_array($db, $supportedDatabases)) { - throw new InvalidArgumentException("Database <$db> is not supported. " . implode(", ", $supportedDatabases) . " are supported."); + throw new InvalidArgumentException("Database <$db> is not supported. " . implode(', ', $supportedDatabases) . ' are supported.'); } $dbUser = $input->getOption('database-user'); @@ -150,8 +119,9 @@ class Install extends Command { $dbHost .= ':' . $dbPort; } if ($input->hasParameterOption('--database-pass')) { - $dbPass = (string) $input->getOption('database-pass'); + $dbPass = (string)$input->getOption('database-pass'); } + $disableAdminUser = (bool)$input->getOption('disable-admin-user'); $adminLogin = $input->getOption('admin-user'); $adminPassword = $input->getOption('admin-pass'); $adminEmail = $input->getOption('admin-email'); @@ -159,31 +129,31 @@ 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."); + throw new InvalidArgumentException('Database name not provided.'); } if (is_null($dbPass)) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question('What is the password to access the database with user <'.$dbUser.'>?'); + $question = new Question('What is the password to access the database with user <' . $dbUser . '>?'); $question->setHidden(true); $question->setHiddenFallback(false); $dbPass = $helper->ask($input, $output, $question); } } - if (is_null($adminPassword)) { + if (!$disableAdminUser && $adminPassword === null) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question('What is the password you like to use for the admin account <'.$adminLogin.'>?'); + $question = new Question('What is the password you like to use for the admin account <' . $adminLogin . '>?'); $question->setHidden(true); $question->setHiddenFallback(false); $adminPassword = $helper->ask($input, $output, $question); } - if ($adminEmail !== null && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) { + if (!$disableAdminUser && $adminEmail !== null && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) { throw new InvalidArgumentException('Invalid e-mail-address <' . $adminEmail . '> for <' . $adminLogin . '>.'); } @@ -193,6 +163,7 @@ class Install extends Command { 'dbpass' => $dbPass, 'dbname' => $dbName, 'dbhost' => $dbHost, + 'admindisable' => $disableAdminUser, 'adminlogin' => $adminLogin, 'adminpass' => $adminPassword, 'adminemail' => $adminEmail, @@ -206,9 +177,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 873744e6f94..f8f19a61993 100644 --- a/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php +++ b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Xheni Myrtaj <xheni@protonmail.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Xheni Myrtaj <myrtajxheni@gmail.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/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Maintenance\Mimetype; @@ -31,17 +12,21 @@ class GenerateMimetypeFileBuilder { /** * Generate mime type list file * - * @param array $aliases + * @param array<string,string> $aliases * @return string */ - public function generateFile(array $aliases): string { + public function generateFile(array $aliases, array $names): string { // Remove comments $aliases = array_filter($aliases, static function ($key) { + // Single digit extensions will be treated as integers + // Let's make sure they are strings + // https://github.com/nextcloud/server/issues/42902 + $key = (string)$key; return !($key === '' || $key[0] === '_'); }, ARRAY_FILTER_USE_KEY); // Fetch all files - $dir = new \DirectoryIterator(\OC::$SERVERROOT.'/core/img/filetypes'); + $dir = new \DirectoryIterator(\OC::$SERVERROOT . '/core/img/filetypes'); $files = []; foreach ($dir as $fileInfo) { @@ -57,7 +42,7 @@ class GenerateMimetypeFileBuilder { // Fetch all themes! $themes = []; - $dirs = new \DirectoryIterator(\OC::$SERVERROOT.'/themes/'); + $dirs = new \DirectoryIterator(\OC::$SERVERROOT . '/themes/'); foreach ($dirs as $dir) { //Valid theme dir if ($dir->isFile() || $dir->isDot()) { @@ -86,6 +71,15 @@ class GenerateMimetypeFileBuilder { sort($themes[$theme]); } + $namesOutput = ''; + foreach ($names as $key => $name) { + if (str_starts_with($key, '_') || trim($name) === '') { + // Skip internal names + continue; + } + $namesOutput .= "'$key': t('core', " . json_encode($name) . "),\n"; + } + //Generate the JS return '/** * This file is automatically generated @@ -98,7 +92,8 @@ class GenerateMimetypeFileBuilder { OC.MimeTypeList={ aliases: ' . json_encode($aliases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . ', files: ' . json_encode($files, JSON_PRETTY_PRINT) . ', - themes: ' . json_encode($themes, JSON_PRETTY_PRINT) . ' + themes: ' . json_encode($themes, JSON_PRETTY_PRINT) . ', + names: {' . $namesOutput . '}, }; '; } diff --git a/core/Command/Maintenance/Mimetype/UpdateDB.php b/core/Command/Maintenance/Mimetype/UpdateDB.php index 212b1994263..4467e89eb32 100644 --- a/core/Command/Maintenance/Mimetype/UpdateDB.php +++ b/core/Command/Maintenance/Mimetype/UpdateDB.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance\Mimetype; @@ -62,6 +45,10 @@ class UpdateDB extends Command { $totalNewMimetypes = 0; foreach ($mappings as $ext => $mimetypes) { + // Single digit extensions will be treated as integers + // Let's make sure they are strings + // https://github.com/nextcloud/server/issues/42902 + $ext = (string)$ext; if ($ext[0] === '_') { // comment continue; @@ -72,21 +59,21 @@ class UpdateDB extends Command { $mimetypeId = $this->mimetypeLoader->getId($mimetype); if (!$existing) { - $output->writeln('Added mimetype "'.$mimetype.'" to database'); + $output->writeln('Added mimetype "' . $mimetype . '" to database'); $totalNewMimetypes++; } if (!$existing || $input->getOption('repair-filecache')) { $touchedFilecacheRows = $this->mimetypeLoader->updateFilecache($ext, $mimetypeId); if ($touchedFilecacheRows > 0) { - $output->writeln('Updated '.$touchedFilecacheRows.' filecache rows for mimetype "'.$mimetype.'"'); + $output->writeln('Updated ' . $touchedFilecacheRows . ' filecache rows for mimetype "' . $mimetype . '"'); } $totalFilecacheUpdates += $touchedFilecacheRows; } } - $output->writeln('Added '.$totalNewMimetypes.' new mimetypes'); - $output->writeln('Updated '.$totalFilecacheUpdates.' filecache rows'); + $output->writeln('Added ' . $totalNewMimetypes . ' new mimetypes'); + $output->writeln('Updated ' . $totalFilecacheUpdates . ' filecache rows'); return 0; } } diff --git a/core/Command/Maintenance/Mimetype/UpdateJS.php b/core/Command/Maintenance/Mimetype/UpdateJS.php index 71145907b9e..2132ff54c6d 100644 --- a/core/Command/Maintenance/Mimetype/UpdateJS.php +++ b/core/Command/Maintenance/Mimetype/UpdateJS.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Xheni Myrtaj <myrtajxheni@gmail.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance\Mimetype; @@ -49,7 +32,8 @@ class UpdateJS extends Command { // Output the JS $generatedMimetypeFile = new GenerateMimetypeFileBuilder(); - file_put_contents(\OC::$SERVERROOT.'/core/js/mimetypelist.js', $generatedMimetypeFile->generateFile($aliases)); + $namings = $this->mimetypeDetector->getAllNamings(); + file_put_contents(\OC::$SERVERROOT . '/core/js/mimetypelist.js', $generatedMimetypeFile->generateFile($aliases, $namings)); $output->writeln('<info>mimetypelist.js is updated'); return 0; diff --git a/core/Command/Maintenance/Mode.php b/core/Command/Maintenance/Mode.php index 685c2fa0837..853e843f57b 100644 --- a/core/Command/Maintenance/Mode.php +++ b/core/Command/Maintenance/Mode.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Michael Weimann <mail@michael-weimann.eu> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author scolebrook <scolebrook@mac.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance; @@ -42,7 +24,8 @@ class Mode extends Command { protected function configure() { $this ->setName('maintenance:mode') - ->setDescription('set maintenance mode') + ->setDescription('Show or toggle maintenance mode status') + ->setHelp('Maintenance mode prevents new logins, locks existing sessions, and disables background jobs.') ->addOption( 'on', null, diff --git a/core/Command/Maintenance/Repair.php b/core/Command/Maintenance/Repair.php index 021e83e0833..f0c88f6811b 100644 --- a/core/Command/Maintenance/Repair.php +++ b/core/Command/Maintenance/Repair.php @@ -1,30 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Temtaime <temtaime@gmail.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance; @@ -82,7 +61,7 @@ class Repair extends Command { $this->repair->addStep($step); } - $apps = $this->appManager->getInstalledApps(); + $apps = $this->appManager->getEnabledApps(); foreach ($apps as $app) { if (!$this->appManager->isEnabledForUser($app)) { continue; @@ -91,7 +70,7 @@ class Repair extends Command { if (!is_array($info)) { continue; } - \OC_App::loadApp($app); + $this->appManager->loadApp($app); $steps = $info['repair-steps']['post-migration']; foreach ($steps as $step) { try { diff --git a/core/Command/Maintenance/RepairShareOwnership.php b/core/Command/Maintenance/RepairShareOwnership.php index a89b9660a8c..16675545afe 100644 --- a/core/Command/Maintenance/RepairShareOwnership.php +++ b/core/Command/Maintenance/RepairShareOwnership.php @@ -3,25 +3,8 @@ 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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Maintenance; @@ -31,6 +14,7 @@ use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -50,7 +34,7 @@ class RepairShareOwnership extends Command { ->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"); + ->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 { @@ -68,15 +52,16 @@ class RepairShareOwnership extends Command { } if ($shares) { - $output->writeln(""); - $output->writeln("Found " . count($shares) . " shares with invalid share owner"); + $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(""); + $output->writeln(''); if (!$noConfirm) { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('Repair these shares? [y/N]', false); @@ -84,10 +69,10 @@ class RepairShareOwnership extends Command { return 0; } } - $output->writeln("Repairing " . count($shares) . " shares"); + $output->writeln('Repairing ' . count($shares) . ' shares'); $this->repairShares($shares); } else { - $output->writeln("Found no shares with invalid share owner"); + $output->writeln('Found no shares with invalid share owner'); } return 0; @@ -102,7 +87,7 @@ class RepairShareOwnership extends Command { $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', 'filecache', 'f', $qb->expr()->eq($qb->expr()->castColumn('s.item_source', IQueryBuilder::PARAM_INT), 'f.fileid')) ->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')) @@ -113,7 +98,7 @@ class RepairShareOwnership extends Command { foreach ($brokenShares as $share) { $found[] = [ - 'shareId' => (int) $share['id'], + 'shareId' => (int)$share['id'], 'fileTarget' => $share['file_target'], 'initiator' => $share['uid_initiator'], 'receiver' => $share['share_with'], @@ -147,7 +132,7 @@ class RepairShareOwnership extends Command { foreach ($brokenShares as $share) { $found[] = [ - 'shareId' => (int) $share['id'], + 'shareId' => (int)$share['id'], 'fileTarget' => $share['file_target'], 'initiator' => $share['uid_initiator'], 'receiver' => $share['share_with'], diff --git a/core/Command/Maintenance/UpdateHtaccess.php b/core/Command/Maintenance/UpdateHtaccess.php index 9243567afb4..eeff3bf8c62 100644 --- a/core/Command/Maintenance/UpdateHtaccess.php +++ b/core/Command/Maintenance/UpdateHtaccess.php @@ -1,28 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Maintenance; +use OC\Setup; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -35,7 +20,7 @@ class UpdateHtaccess extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - if (\OC\Setup::updateHtaccess()) { + if (Setup::updateHtaccess()) { $output->writeln('.htaccess has been updated'); return 0; } else { diff --git a/core/Command/Maintenance/UpdateTheme.php b/core/Command/Maintenance/UpdateTheme.php index 859aaa12d4f..3fbcb546cca 100644 --- a/core/Command/Maintenance/UpdateTheme.php +++ b/core/Command/Maintenance/UpdateTheme.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Maintenance; diff --git a/core/Command/Memcache/DistributedClear.php b/core/Command/Memcache/DistributedClear.php new file mode 100644 index 00000000000..424f21f1e81 --- /dev/null +++ b/core/Command/Memcache/DistributedClear.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Memcache; + +use OC\Core\Command\Base; +use OCP\ICacheFactory; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class DistributedClear extends Base { + public function __construct( + protected ICacheFactory $cacheFactory, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('memcache:distributed:clear') + ->setDescription('Clear values from the distributed memcache') + ->addOption('prefix', null, InputOption::VALUE_REQUIRED, 'Only remove keys matching the prefix'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $cache = $this->cacheFactory->createDistributed(); + $prefix = $input->getOption('prefix'); + if ($cache->clear($prefix)) { + if ($prefix) { + $output->writeln('<info>Distributed cache matching prefix ' . $prefix . ' cleared</info>'); + } else { + $output->writeln('<info>Distributed cache cleared</info>'); + } + return 0; + } else { + $output->writeln('<error>Failed to clear cache</error>'); + return 1; + } + } +} diff --git a/core/Command/Memcache/DistributedDelete.php b/core/Command/Memcache/DistributedDelete.php new file mode 100644 index 00000000000..ae0855acb03 --- /dev/null +++ b/core/Command/Memcache/DistributedDelete.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Memcache; + +use OC\Core\Command\Base; +use OCP\ICacheFactory; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class DistributedDelete extends Base { + public function __construct( + protected ICacheFactory $cacheFactory, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('memcache:distributed:delete') + ->setDescription('Delete a value in the distributed memcache') + ->addArgument('key', InputArgument::REQUIRED, 'The key to delete'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $cache = $this->cacheFactory->createDistributed(); + $key = $input->getArgument('key'); + if ($cache->remove($key)) { + $output->writeln('<info>Distributed cache key <info>' . $key . '</info> deleted</info>'); + return 0; + } else { + $output->writeln('<error>Failed to delete cache key ' . $key . '</error>'); + return 1; + } + } +} diff --git a/core/Command/Memcache/DistributedGet.php b/core/Command/Memcache/DistributedGet.php new file mode 100644 index 00000000000..bf1b00d312d --- /dev/null +++ b/core/Command/Memcache/DistributedGet.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Memcache; + +use OC\Core\Command\Base; +use OCP\ICacheFactory; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class DistributedGet extends Base { + public function __construct( + protected ICacheFactory $cacheFactory, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('memcache:distributed:get') + ->setDescription('Get a value from the distributed memcache') + ->addArgument('key', InputArgument::REQUIRED, 'The key to retrieve'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $cache = $this->cacheFactory->createDistributed(); + $key = $input->getArgument('key'); + + $value = $cache->get($key); + $this->writeMixedInOutputFormat($input, $output, $value); + return 0; + } +} diff --git a/core/Command/Memcache/DistributedSet.php b/core/Command/Memcache/DistributedSet.php new file mode 100644 index 00000000000..0f31c22f730 --- /dev/null +++ b/core/Command/Memcache/DistributedSet.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Memcache; + +use OC\Core\Command\Base; +use OC\Core\Command\Config\System\CastHelper; +use OCP\ICacheFactory; +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 DistributedSet extends Base { + public function __construct( + protected ICacheFactory $cacheFactory, + private CastHelper $castHelper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('memcache:distributed:set') + ->setDescription('Set a value in the distributed memcache') + ->addArgument('key', InputArgument::REQUIRED, 'The key to set') + ->addArgument('value', InputArgument::REQUIRED, 'The value to set') + ->addOption( + 'type', + null, + InputOption::VALUE_REQUIRED, + 'Value type [string, integer, float, boolean, json, null]', + 'string' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $cache = $this->cacheFactory->createDistributed(); + $key = $input->getArgument('key'); + $value = $input->getArgument('value'); + $type = $input->getOption('type'); + ['value' => $value, 'readable-value' => $readable] = $this->castHelper->castValue($value, $type); + if ($cache->set($key, $value)) { + $output->writeln('Distributed cache key <info>' . $key . '</info> set to <info>' . $readable . '</info>'); + return 0; + } else { + $output->writeln('<error>Failed to set cache key ' . $key . '</error>'); + return 1; + } + } +} diff --git a/core/Command/Memcache/RedisCommand.php b/core/Command/Memcache/RedisCommand.php new file mode 100644 index 00000000000..429dd28f3b3 --- /dev/null +++ b/core/Command/Memcache/RedisCommand.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Memcache; + +use OC\Core\Command\Base; +use OC\RedisFactory; +use OCP\ICertificateManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class RedisCommand extends Base { + public function __construct( + protected ICertificateManager $certificateManager, + protected RedisFactory $redisFactory, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('memcache:redis:command') + ->setDescription('Send raw redis command to the configured redis server') + ->addArgument('redis-command', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'The command to run'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $command = $input->getArgument('redis-command'); + if (!$this->redisFactory->isAvailable()) { + $output->writeln('<error>No redis server configured</error>'); + return 1; + } + try { + $redis = $this->redisFactory->getInstance(); + } catch (\Exception $e) { + $output->writeln('Failed to connect to redis: ' . $e->getMessage()); + return 1; + } + + $redis->setOption(\Redis::OPT_REPLY_LITERAL, true); + $result = $redis->rawCommand(...$command); + if ($result === false) { + $output->writeln('<error>Redis command failed</error>'); + return 1; + } + $output->writeln($result); + return 0; + } +} diff --git a/core/Command/Preview/Cleanup.php b/core/Command/Preview/Cleanup.php new file mode 100644 index 00000000000..dad981a5243 --- /dev/null +++ b/core/Command/Preview/Cleanup.php @@ -0,0 +1,88 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Preview; + +use OC\Core\Command\Base; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Cleanup extends Base { + + public function __construct( + private IRootFolder $rootFolder, + private LoggerInterface $logger, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('preview:cleanup') + ->setDescription('Removes existing preview files'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $appDataFolder = $this->rootFolder->get($this->rootFolder->getAppDataDirectoryName()); + + if (!$appDataFolder instanceof Folder) { + $this->logger->error("Previews can't be removed: appdata is not a folder"); + $output->writeln("Previews can't be removed: appdata is not a folder"); + return 1; + } + + /** @var Folder $previewFolder */ + $previewFolder = $appDataFolder->get('preview'); + + } catch (NotFoundException $e) { + $this->logger->error("Previews can't be removed: appdata folder can't be found", ['exception' => $e]); + $output->writeln("Previews can't be removed: preview folder isn't deletable"); + return 1; + } + + if (!$previewFolder->isDeletable()) { + $this->logger->error("Previews can't be removed: preview folder isn't deletable"); + $output->writeln("Previews can't be removed: preview folder isn't deletable"); + return 1; + } + + try { + $previewFolder->delete(); + $this->logger->debug('Preview folder deleted'); + $output->writeln('Preview folder deleted', OutputInterface::VERBOSITY_VERBOSE); + } catch (NotFoundException $e) { + $output->writeln("Previews weren't deleted: preview folder was not found while deleting it"); + $this->logger->error("Previews weren't deleted: preview folder was not found while deleting it", ['exception' => $e]); + return 1; + } catch (NotPermittedException $e) { + $output->writeln("Previews weren't deleted: you don't have the permission to delete preview folder"); + $this->logger->error("Previews weren't deleted: you don't have the permission to delete preview folder", ['exception' => $e]); + return 1; + } + + try { + $appDataFolder->newFolder('preview'); + $this->logger->debug('Preview folder recreated'); + $output->writeln('Preview folder recreated', OutputInterface::VERBOSITY_VERBOSE); + } catch (NotPermittedException $e) { + $output->writeln("Preview folder was deleted, but you don't have the permission to create preview folder"); + $this->logger->error("Preview folder was deleted, but you don't have the permission to create preview folder", ['exception' => $e]); + return 1; + } + + $output->writeln('Previews removed'); + return 0; + } +} diff --git a/core/Command/Preview/Generate.php b/core/Command/Preview/Generate.php index a885d224fc7..222c42f613b 100644 --- a/core/Command/Preview/Generate.php +++ b/core/Command/Preview/Generate.php @@ -2,23 +2,8 @@ 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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Preview; @@ -48,38 +33,38 @@ class Generate extends Command { $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); + ->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) { + $fileInput = $input->getArgument('file'); + $sizes = $input->getOption('size'); + $sizes = array_map(function (string $size) use ($output) { if (str_contains($size, 'x')) { $sizeParts = explode('x', $size, 2); } else { $sizeParts = [$size, $size]; } - if (!is_numeric($sizeParts[0]) || !is_numeric($sizeParts[1])) { + if (!is_numeric($sizeParts[0]) || !is_numeric($sizeParts[1] ?? null)) { $output->writeln("<error>Invalid size $size</error>"); return null; } - return array_map("intval", $sizeParts); + return array_map('intval', $sizeParts); }, $sizes); if (in_array(null, $sizes)) { return 1; } - $mode = $input->getOption("mode"); + $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"); + $crop = $input->getOption('crop'); $file = $this->getFile($fileInput); if (!$file) { $output->writeln("<error>File $fileInput not found</error>"); @@ -91,7 +76,7 @@ class Generate extends Command { } if (!$this->previewManager->isAvailable($file)) { - $output->writeln("<error>No preview generator available for file of type" . $file->getMimetype() . "</error>"); + $output->writeln('<error>No preview generator available for file of type' . $file->getMimetype() . '</error>'); return 1; } @@ -106,9 +91,9 @@ class Generate extends Command { $this->previewManager->generatePreviews($file, $specifications); if (count($specifications) > 1) { - $output->writeln("generated <info>" . count($specifications) . "</info> previews"); + $output->writeln('generated <info>' . count($specifications) . '</info> previews'); } else { - $output->writeln("preview generated"); + $output->writeln('preview generated'); } return 0; } @@ -121,11 +106,7 @@ class Generate extends Command { } $mount = $mounts[0]; $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID()); - $nodes = $userFolder->getById((int)$fileInput); - if (!$nodes) { - return null; - } - return $nodes[0]; + return $userFolder->getFirstNodeById((int)$fileInput); } else { try { return $this->rootFolder->get($fileInput); diff --git a/core/Command/Preview/Repair.php b/core/Command/Preview/Repair.php index fcdea9c20c8..a92a4cf8ed0 100644 --- a/core/Command/Preview/Repair.php +++ b/core/Command/Preview/Repair.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Morris Jobke <hey@morrisjobke.de> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Preview; @@ -37,6 +19,7 @@ use OCP\Lock\LockedException; use Psr\Log\LoggerInterface; 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\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -77,11 +60,11 @@ class Repair extends Command { $thresholdInMiB = round($this->memoryTreshold / 1024 / 1024, 1); $output->writeln("Memory limit is $limitInMiB MiB"); $output->writeln("Memory threshold is $thresholdInMiB MiB"); - $output->writeln(""); + $output->writeln(''); $memoryCheckEnabled = true; } else { - $output->writeln("No memory limit in place - disabled memory check. Set a PHP memory limit to automatically stop the execution of this migration script once memory consumption is close to this limit."); - $output->writeln(""); + $output->writeln('No memory limit in place - disabled memory check. Set a PHP memory limit to automatically stop the execution of this migration script once memory consumption is close to this limit.'); + $output->writeln(''); $memoryCheckEnabled = false; } @@ -90,20 +73,20 @@ class Repair extends Command { if ($dryMode) { - $output->writeln("INFO: The migration is run in dry mode and will not modify anything."); - $output->writeln(""); + $output->writeln('INFO: The migration is run in dry mode and will not modify anything.'); + $output->writeln(''); } elseif ($deleteMode) { - $output->writeln("WARN: The migration will _DELETE_ old previews."); - $output->writeln(""); + $output->writeln('WARN: The migration will _DELETE_ old previews.'); + $output->writeln(''); } $instanceId = $this->config->getSystemValueString('instanceid'); - $output->writeln("This will migrate all previews from the old preview location to the new one."); + $output->writeln('This will migrate all previews from the old preview location to the new one.'); $output->writeln(''); $output->writeln('Fetching previews that need to be migrated …'); - /** @var \OCP\Files\Folder $currentPreviewFolder */ + /** @var Folder $currentPreviewFolder */ $currentPreviewFolder = $this->rootFolder->get("appdata_$instanceId/preview"); $directoryListing = $currentPreviewFolder->getDirectoryListing(); @@ -140,17 +123,18 @@ class Repair extends Command { } if ($total === 0) { - $output->writeln("All previews are already migrated."); + $output->writeln('All previews are already migrated.'); return 0; } $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 will finish the current batch and then stop the migration. This migration can then just be started and it will continue."); + $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 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.'); } else { + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion('<info>Should the migration be started? (y/[n]) </info>', false); @@ -162,12 +146,12 @@ class Repair extends Command { // register the SIGINT listener late in here to be able to exit in the early process of this command pcntl_signal(SIGINT, [$this, 'sigIntHandler']); - $output->writeln(""); - $output->writeln(""); + $output->writeln(''); + $output->writeln(''); $section1 = $output->section(); $section2 = $output->section(); $progressBar = new ProgressBar($section2, $total); - $progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%"); + $progressBar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%'); $time = (new \DateTime())->format('H:i:s'); $progressBar->setMessage("$time Starting …"); $progressBar->maxSecondsBetweenRedraws(0.2); @@ -209,10 +193,10 @@ class Repair extends Command { $memoryUsage = memory_get_usage(); if ($memoryCheckEnabled && $memoryUsage > $this->memoryTreshold) { - $section1->writeln(""); - $section1->writeln(""); - $section1->writeln(""); - $section1->writeln(" Stopped process 25 MB before reaching the memory limit to avoid a hard crash."); + $section1->writeln(''); + $section1->writeln(''); + $section1->writeln(''); + $section1->writeln(' Stopped process 25 MB before reaching the memory limit to avoid a hard crash.'); $time = (new \DateTime())->format('H:i:s'); $section1->writeln("$time Reached memory limit and stopped to avoid hard crash."); return 1; @@ -223,7 +207,7 @@ class Repair extends Command { $section1->writeln(" Locking \"$lockName\" …", OutputInterface::VERBOSITY_VERBOSE); $this->lockingProvider->acquireLock($lockName, ILockingProvider::LOCK_EXCLUSIVE); } catch (LockedException $e) { - $section1->writeln(" Skipping because it is locked - another process seems to work on this …"); + $section1->writeln(' Skipping because it is locked - another process seems to work on this …'); continue; } @@ -291,14 +275,14 @@ class Repair extends Command { } $this->lockingProvider->releaseLock($lockName, ILockingProvider::LOCK_EXCLUSIVE); - $section1->writeln(" Unlocked", OutputInterface::VERBOSITY_VERBOSE); + $section1->writeln(' Unlocked', OutputInterface::VERBOSITY_VERBOSE); $section1->writeln(" Finished migrating previews of file with fileId $name …"); $progressBar->advance(); } $progressBar->finish(); - $output->writeln(""); + $output->writeln(''); return 0; } diff --git a/core/Command/Preview/ResetRenderedTexts.php b/core/Command/Preview/ResetRenderedTexts.php index ec57f632ac9..4cae315e48b 100644 --- a/core/Command/Preview/ResetRenderedTexts.php +++ b/core/Command/Preview/ResetRenderedTexts.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2021, Daniel Calviño Sánchez <danxuliu@gmail.com> - * - * @author Daniel Calviño Sánchez <danxuliu@gmail.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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Preview; @@ -137,7 +120,7 @@ class ResetRenderedTexts extends Command { $qb->select('path', 'mimetype') ->from('filecache') ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($this->previewFolder->getId()))); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -170,7 +153,7 @@ class ResetRenderedTexts extends Command { ) ); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); while ($row = $cursor->fetch()) { yield $row; diff --git a/core/Command/Router/ListRoutes.php b/core/Command/Router/ListRoutes.php new file mode 100644 index 00000000000..8932b549a65 --- /dev/null +++ b/core/Command/Router/ListRoutes.php @@ -0,0 +1,129 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Router; + +use OC\Core\Command\Base; +use OC\Route\Router; +use OCP\App\AppPathNotFoundException; +use OCP\App\IAppManager; +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 ListRoutes extends Base { + + public function __construct( + protected IAppManager $appManager, + protected Router $router, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('router:list') + ->setDescription('Find the target of a route or all routes of an app') + ->addArgument( + 'app', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'Only list routes of these apps', + ) + ->addOption( + 'ocs', + null, + InputOption::VALUE_NONE, + 'Only list OCS routes', + ) + ->addOption( + 'index', + null, + InputOption::VALUE_NONE, + 'Only list index.php routes', + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $apps = $input->getArgument('app'); + if (empty($apps)) { + $this->router->loadRoutes(); + } else { + foreach ($apps as $app) { + if ($app === 'core') { + $this->router->loadRoutes($app, false); + continue; + } + + try { + $this->appManager->getAppPath($app); + } catch (AppPathNotFoundException $e) { + $output->writeln('<comment>App ' . $app . ' not found</comment>'); + return self::FAILURE; + } + + if (!$this->appManager->isEnabledForAnyone($app)) { + $output->writeln('<comment>App ' . $app . ' is not enabled</comment>'); + return self::FAILURE; + } + + $this->router->loadRoutes($app, true); + } + } + + $ocsOnly = $input->getOption('ocs'); + $indexOnly = $input->getOption('index'); + + $rows = []; + $collection = $this->router->getRouteCollection(); + foreach ($collection->all() as $routeName => $route) { + if (str_starts_with($routeName, 'ocs.')) { + if ($indexOnly) { + continue; + } + $routeName = substr($routeName, 4); + } elseif ($ocsOnly) { + continue; + } + + $path = $route->getPath(); + if (str_starts_with($path, '/ocsapp/')) { + $path = '/ocs/v2.php/' . substr($path, strlen('/ocsapp/')); + } + $row = [ + 'route' => $routeName, + 'request' => implode(', ', $route->getMethods()), + 'path' => $path, + ]; + + if ($output->isVerbose()) { + $row['requirements'] = json_encode($route->getRequirements()); + } + + $rows[] = $row; + } + + usort($rows, static function (array $a, array $b): int { + $aRoute = $a['route']; + if (str_starts_with($aRoute, 'ocs.')) { + $aRoute = substr($aRoute, 4); + } + $bRoute = $b['route']; + if (str_starts_with($bRoute, 'ocs.')) { + $bRoute = substr($bRoute, 4); + } + return $aRoute <=> $bRoute; + }); + + $this->writeTableInOutputFormat($input, $output, $rows); + return self::SUCCESS; + } +} diff --git a/core/Command/Router/MatchRoute.php b/core/Command/Router/MatchRoute.php new file mode 100644 index 00000000000..3b90463c7b2 --- /dev/null +++ b/core/Command/Router/MatchRoute.php @@ -0,0 +1,100 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\Router; + +use OC\Core\Command\Base; +use OC\Route\Router; +use OCP\App\IAppManager; +use OCP\Server; +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\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RequestContext; + +class MatchRoute extends Base { + + public function __construct( + private Router $router, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('router:match') + ->setDescription('Match a URL to the target route') + ->addArgument( + 'path', + InputArgument::REQUIRED, + 'Path of the request', + ) + ->addOption( + 'method', + null, + InputOption::VALUE_REQUIRED, + 'HTTP method', + 'GET', + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $context = new RequestContext(method: strtoupper($input->getOption('method'))); + $this->router->setContext($context); + + $path = $input->getArgument('path'); + if (str_starts_with($path, '/index.php/')) { + $path = substr($path, 10); + } + if (str_starts_with($path, '/ocs/v1.php/') || str_starts_with($path, '/ocs/v2.php/')) { + $path = '/ocsapp' . substr($path, strlen('/ocs/v2.php')); + } + + try { + $route = $this->router->findMatchingRoute($path); + } catch (MethodNotAllowedException) { + $output->writeln('<error>Method not allowed on this path</error>'); + return self::FAILURE; + } catch (ResourceNotFoundException) { + $output->writeln('<error>Path not matched</error>'); + if (preg_match('/\/apps\/([^\/]+)\//', $path, $matches)) { + $appManager = Server::get(IAppManager::class); + if (!$appManager->isEnabledForAnyone($matches[1])) { + $output->writeln(''); + $output->writeln('<comment>App ' . $matches[1] . ' is not enabled</comment>'); + } + } + return self::FAILURE; + } + + $row = [ + 'route' => $route['_route'], + 'appid' => $route['caller'][0] ?? null, + 'controller' => $route['caller'][1] ?? null, + 'method' => $route['caller'][2] ?? null, + ]; + + if ($output->isVerbose()) { + $route = $this->router->getRouteCollection()->get($row['route']); + $row['path'] = $route->getPath(); + if (str_starts_with($row['path'], '/ocsapp/')) { + $row['path'] = '/ocs/v2.php/' . substr($row['path'], strlen('/ocsapp/')); + } + $row['requirements'] = json_encode($route->getRequirements()); + } + + $this->writeTableInOutputFormat($input, $output, [$row]); + return self::SUCCESS; + } +} diff --git a/core/Command/Security/BruteforceAttempts.php b/core/Command/Security/BruteforceAttempts.php index 9237078339d..d5fa0a284fd 100644 --- a/core/Command/Security/BruteforceAttempts.php +++ b/core/Command/Security/BruteforceAttempts.php @@ -2,25 +2,8 @@ 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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Security; @@ -41,11 +24,11 @@ class BruteforceAttempts extends Base { parent::configure(); $this ->setName('security:bruteforce:attempts') - ->setDescription('resets bruteforce attempts for given IP address') + ->setDescription('Show bruteforce attempts status for a given IP address') ->addArgument( 'ipaddress', InputArgument::REQUIRED, - 'IP address for which the attempts are to be reset', + 'IP address for which the attempts status is to be shown', ) ->addArgument( 'action', @@ -67,11 +50,11 @@ class BruteforceAttempts extends Base { 'bypass-listed' => $this->throttler->isBypassListed($ip), 'attempts' => $this->throttler->getAttempts( $ip, - (string) $input->getArgument('action'), + (string)$input->getArgument('action'), ), 'delay' => $this->throttler->getDelay( $ip, - (string) $input->getArgument('action'), + (string)$input->getArgument('action'), ), ]; diff --git a/core/Command/Security/BruteforceResetAttempts.php b/core/Command/Security/BruteforceResetAttempts.php index 40d7c6848b2..6987c0ef682 100644 --- a/core/Command/Security/BruteforceResetAttempts.php +++ b/core/Command/Security/BruteforceResetAttempts.php @@ -2,26 +2,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Johannes Riedel (johannes@johannes-riedel.de) - * - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Riedel <joeried@users.noreply.github.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/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\Security; diff --git a/core/Command/Security/ExportCertificates.php b/core/Command/Security/ExportCertificates.php new file mode 100644 index 00000000000..dcf34d4bce4 --- /dev/null +++ b/core/Command/Security/ExportCertificates.php @@ -0,0 +1,35 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ +declare(strict_types=1); + +namespace OC\Core\Command\Security; + +use OC\Core\Command\Base; +use OCP\ICertificateManager; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ExportCertificates extends Base { + public function __construct( + protected ICertificateManager $certificateManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('security:certificates:export') + ->setDescription('export the certificate bundle'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $bundlePath = $this->certificateManager->getAbsoluteBundlePath(); + $bundle = file_get_contents($bundlePath); + $output->writeln($bundle); + return 0; + } +} diff --git a/core/Command/Security/ImportCertificate.php b/core/Command/Security/ImportCertificate.php index a7e9bd94e4a..b23612baeb1 100644 --- a/core/Command/Security/ImportCertificate.php +++ b/core/Command/Security/ImportCertificate.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Security; diff --git a/core/Command/Security/ListCertificates.php b/core/Command/Security/ListCertificates.php index 96063fb5b61..cf1874a09d3 100644 --- a/core/Command/Security/ListCertificates.php +++ b/core/Command/Security/ListCertificates.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Security; @@ -26,16 +11,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 IL10N $l; + public function __construct( protected ICertificateManager $certificateManager, - protected IL10N $l, + 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 7dcd2d02604..48062724d52 100644 --- a/core/Command/Security/RemoveCertificate.php +++ b/core/Command/Security/RemoveCertificate.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Carla Schroder <carla@owncloud.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\Security; diff --git a/core/Command/SetupChecks.php b/core/Command/SetupChecks.php index e6e54fbcf22..6ef67726839 100644 --- a/core/Command/SetupChecks.php +++ b/core/Command/SetupChecks.php @@ -3,28 +3,13 @@ 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/> - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command; +use OCP\RichObjectStrings\IRichTextFormatter; use OCP\SetupCheck\ISetupCheckManager; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -32,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; class SetupChecks extends Base { public function __construct( private ISetupCheckManager $setupCheckManager, + private IRichTextFormatter $richTextFormatter, ) { parent::__construct(); } @@ -45,29 +31,6 @@ class SetupChecks extends Base { ; } - /** - * @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')) { @@ -95,15 +58,15 @@ class SetupChecks extends Base { $description = $check->getDescription(); $descriptionParameters = $check->getDescriptionParameters(); if ($description !== null && $descriptionParameters !== null) { - $description = $this->richToParsed($description, $descriptionParameters); + $description = $this->richTextFormatter->richToParsed($description, $descriptionParameters); } $output->writeln( - "\t\t". - ($styleTag !== null ? "<{$styleTag}>" : ''). - "{$emoji} ". - ($check->getName() ?? $check::class). - ($description !== null ? ': '.$description : ''). - ($styleTag !== null ? "</{$styleTag}>" : ''), + "\t\t" + . ($styleTag !== null ? "<{$styleTag}>" : '') + . "{$emoji} " + . ($check->getName() ?? $check::class) + . ($description !== null ? ': ' . $description : '') + . ($styleTag !== null ? "</{$styleTag}>" : ''), $verbosity ); } diff --git a/core/Command/Status.php b/core/Command/Status.php index 57b831c7eaa..a00d4a94658 100644 --- a/core/Command/Status.php +++ b/core/Command/Status.php @@ -1,32 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command; -use OC_Util; use OCP\Defaults; use OCP\IConfig; +use OCP\ServerVersion; use OCP\Util; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -36,6 +19,7 @@ class Status extends Base { public function __construct( private IConfig $config, private Defaults $themingDefaults, + private ServerVersion $serverVersion, ) { parent::__construct('status'); } @@ -58,8 +42,8 @@ class Status extends Base { $needUpgrade = Util::needUpgrade(); $values = [ 'installed' => $this->config->getSystemValueBool('installed', false), - 'version' => implode('.', Util::getVersion()), - 'versionstring' => OC_Util::getVersionString(), + 'version' => implode('.', $this->serverVersion->getVersion()), + 'versionstring' => $this->serverVersion->getVersionString(), 'edition' => '', 'maintenance' => $maintenanceMode, 'needsDbUpgrade' => $needUpgrade, diff --git a/core/Command/SystemTag/Add.php b/core/Command/SystemTag/Add.php index 067cd00118a..df8b507b07d 100644 --- a/core/Command/SystemTag/Add.php +++ b/core/Command/SystemTag/Add.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, hosting.de, Johannes Leuker <developers@hosting.de> - * - * @author Johannes Leuker <j.leuker@hosting.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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\SystemTag; @@ -90,7 +74,7 @@ class Add extends Base { ]); return 0; } catch (TagAlreadyExistsException $e) { - $output->writeln('<error>'.$e->getMessage().'</error>'); + $output->writeln('<error>' . $e->getMessage() . '</error>'); return 2; } } diff --git a/core/Command/SystemTag/Delete.php b/core/Command/SystemTag/Delete.php index ed893ae037c..f657f4473ab 100644 --- a/core/Command/SystemTag/Delete.php +++ b/core/Command/SystemTag/Delete.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, hosting.de, Johannes Leuker <developers@hosting.de> - * - * @author Johannes Leuker <j.leuker@hosting.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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\SystemTag; diff --git a/core/Command/SystemTag/Edit.php b/core/Command/SystemTag/Edit.php index 111dc500e79..09c662e58e9 100644 --- a/core/Command/SystemTag/Edit.php +++ b/core/Command/SystemTag/Edit.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, hosting.de, Johannes Leuker <developers@hosting.de> - * - * @author Johannes Leuker <j.leuker@hosting.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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\SystemTag; @@ -57,6 +41,12 @@ class Edit extends Base { null, InputOption::VALUE_OPTIONAL, 'sets the access control level (public, restricted, invisible)', + ) + ->addOption( + 'color', + null, + InputOption::VALUE_OPTIONAL, + 'set the tag color', ); } @@ -97,15 +87,30 @@ class Edit extends Base { } } + $color = $tag->getColor(); + if ($input->hasOption('color')) { + $color = $input->getOption('color'); + if (substr($color, 0, 1) === '#') { + $color = substr($color, 1); + } + + if ($input->getOption('color') === '') { + $color = null; + } elseif (strlen($color) !== 6 || !ctype_xdigit($color)) { + $output->writeln('<error>Color must be a 6-digit hexadecimal value</error>'); + return 2; + } + } + try { - $this->systemTagManager->updateTag($input->getArgument('id'), $name, $userVisible, $userAssignable); - $output->writeln('<info>Tag updated ("' . $name . '", '. $userVisible . ', ' . $userAssignable . ')</info>'); + $this->systemTagManager->updateTag($input->getArgument('id'), $name, $userVisible, $userAssignable, $color); + $output->writeln('<info>Tag updated ("' . $name . '", ' . json_encode($userVisible) . ', ' . json_encode($userAssignable) . ', "' . ($color ? "#$color" : '') . '")</info>'); return 0; } catch (TagNotFoundException $e) { $output->writeln('<error>Tag not found</error>'); return 1; } catch (TagAlreadyExistsException $e) { - $output->writeln('<error>'.$e->getMessage().'</error>'); + $output->writeln('<error>' . $e->getMessage() . '</error>'); return 2; } } diff --git a/core/Command/SystemTag/ListCommand.php b/core/Command/SystemTag/ListCommand.php index c0f4eba241c..2c6435d6faf 100644 --- a/core/Command/SystemTag/ListCommand.php +++ b/core/Command/SystemTag/ListCommand.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2021, hosting.de, Johannes Leuker <developers@hosting.de> - * - * @author Johannes Leuker <j.leuker@hosting.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/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\SystemTag; diff --git a/core/Command/TaskProcessing/Cleanup.php b/core/Command/TaskProcessing/Cleanup.php new file mode 100644 index 00000000000..2ed2cbdec94 --- /dev/null +++ b/core/Command/TaskProcessing/Cleanup.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OC\TaskProcessing\Db\TaskMapper; +use OC\TaskProcessing\Manager; +use OCP\Files\AppData\IAppDataFactory; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Cleanup extends Base { + private \OCP\Files\IAppData $appData; + + public function __construct( + protected Manager $taskProcessingManager, + private TaskMapper $taskMapper, + private LoggerInterface $logger, + IAppDataFactory $appDataFactory, + ) { + parent::__construct(); + $this->appData = $appDataFactory->get('core'); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:cleanup') + ->setDescription('cleanup old tasks') + ->addArgument( + 'maxAgeSeconds', + InputArgument::OPTIONAL, + // default is not defined as an argument default value because we want to show a nice "4 months" value + 'delete tasks that are older than this number of seconds, defaults to ' . Manager::MAX_TASK_AGE_SECONDS . ' (4 months)', + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $maxAgeSeconds = $input->getArgument('maxAgeSeconds') ?? Manager::MAX_TASK_AGE_SECONDS; + $output->writeln('<comment>Cleanup up tasks older than ' . $maxAgeSeconds . ' seconds and the related output files</comment>'); + + $taskIdsToCleanup = []; + try { + $fileCleanupGenerator = $this->taskProcessingManager->cleanupTaskProcessingTaskFiles($maxAgeSeconds); + foreach ($fileCleanupGenerator as $cleanedUpEntry) { + $output->writeln( + "<info>\t - " . 'Deleted appData/core/TaskProcessing/' . $cleanedUpEntry['file_name'] + . ' (fileId: ' . $cleanedUpEntry['file_id'] . ', taskId: ' . $cleanedUpEntry['task_id'] . ')</info>' + ); + } + $taskIdsToCleanup = $fileCleanupGenerator->getReturn(); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); + $output->writeln('<warning>Failed to delete stale task processing tasks files</warning>'); + } + try { + $deletedTaskCount = $this->taskMapper->deleteOlderThan($maxAgeSeconds); + foreach ($taskIdsToCleanup as $taskId) { + $output->writeln("<info>\t - " . 'Deleted task ' . $taskId . ' from the database</info>'); + } + $output->writeln("<comment>\t - " . 'Deleted ' . $deletedTaskCount . ' tasks from the database</comment>'); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + $output->writeln('<warning>Failed to delete stale task processing tasks</warning>'); + } + try { + $textToImageDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'), $maxAgeSeconds); + foreach ($textToImageDeletedFileNames as $entry) { + $output->writeln("<info>\t - " . 'Deleted appData/core/text2image/' . $entry . '</info>'); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + try { + $audioToTextDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'), $maxAgeSeconds); + foreach ($audioToTextDeletedFileNames as $entry) { + $output->writeln("<info>\t - " . 'Deleted appData/core/audio2text/' . $entry . '</info>'); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + + return 0; + } +} diff --git a/core/Command/TaskProcessing/EnabledCommand.php b/core/Command/TaskProcessing/EnabledCommand.php new file mode 100644 index 00000000000..a99e3e001b9 --- /dev/null +++ b/core/Command/TaskProcessing/EnabledCommand.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\IAppConfig; +use OCP\TaskProcessing\IManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class EnabledCommand extends Base { + public function __construct( + protected IManager $taskProcessingManager, + private IAppConfig $appConfig, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task-type:set-enabled') + ->setDescription('Enable or disable a task type') + ->addArgument( + 'task-type-id', + InputArgument::REQUIRED, + 'ID of the task type to configure' + ) + ->addArgument( + 'enabled', + InputArgument::REQUIRED, + 'status of the task type availability. Set 1 to enable and 0 to disable.' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $enabled = (bool)$input->getArgument('enabled'); + $taskType = $input->getArgument('task-type-id'); + $json = $this->appConfig->getValueString('core', 'ai.taskprocessing_type_preferences', lazy: true); + try { + if ($json === '') { + $taskTypeSettings = []; + } else { + $taskTypeSettings = json_decode($json, true, flags: JSON_THROW_ON_ERROR); + } + + $taskTypeSettings[$taskType] = $enabled; + + $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskTypeSettings), lazy: true); + $this->writeArrayInOutputFormat($input, $output, $taskTypeSettings); + return 0; + } catch (\JsonException $e) { + throw new \JsonException('Error in TaskType DB entry'); + } + + } +} diff --git a/core/Command/TaskProcessing/GetCommand.php b/core/Command/TaskProcessing/GetCommand.php new file mode 100644 index 00000000000..f97556281a1 --- /dev/null +++ b/core/Command/TaskProcessing/GetCommand.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\TaskProcessing\IManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class GetCommand extends Base { + public function __construct( + protected IManager $taskProcessingManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:get') + ->setDescription('Display all information for a specific task') + ->addArgument( + 'task-id', + InputArgument::REQUIRED, + 'ID of the task to display' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $taskId = (int)$input->getArgument('task-id'); + $task = $this->taskProcessingManager->getTask($taskId); + $jsonTask = $task->jsonSerialize(); + $jsonTask['error_message'] = $task->getErrorMessage(); + $this->writeArrayInOutputFormat($input, $output, $jsonTask); + return 0; + } +} diff --git a/core/Command/TaskProcessing/ListCommand.php b/core/Command/TaskProcessing/ListCommand.php new file mode 100644 index 00000000000..50a694c1a6e --- /dev/null +++ b/core/Command/TaskProcessing/ListCommand.php @@ -0,0 +1,98 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\TaskProcessing\IManager; +use OCP\TaskProcessing\Task; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ListCommand extends Base { + public function __construct( + protected IManager $taskProcessingManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:list') + ->setDescription('list tasks') + ->addOption( + 'userIdFilter', + 'u', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one user ID' + ) + ->addOption( + 'type', + 't', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one task type' + ) + ->addOption( + 'appId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one app ID' + ) + ->addOption( + 'customId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one custom ID' + ) + ->addOption( + 'status', + 's', + InputOption::VALUE_OPTIONAL, + 'only get the tasks that have a specific status (STATUS_UNKNOWN=0, STATUS_SCHEDULED=1, STATUS_RUNNING=2, STATUS_SUCCESSFUL=3, STATUS_FAILED=4, STATUS_CANCELLED=5)' + ) + ->addOption( + 'scheduledAfter', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that were scheduled after a specific date (Unix timestamp)' + ) + ->addOption( + 'endedBefore', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that ended before a specific date (Unix timestamp)' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userIdFilter = $input->getOption('userIdFilter'); + if ($userIdFilter === null) { + $userIdFilter = ''; + } elseif ($userIdFilter === '') { + $userIdFilter = null; + } + $type = $input->getOption('type'); + $appId = $input->getOption('appId'); + $customId = $input->getOption('customId'); + $status = $input->getOption('status'); + $scheduledAfter = $input->getOption('scheduledAfter'); + $endedBefore = $input->getOption('endedBefore'); + + $tasks = $this->taskProcessingManager->getTasks($userIdFilter, $type, $appId, $customId, $status, $scheduledAfter, $endedBefore); + $arrayTasks = array_map(static function (Task $task) { + $jsonTask = $task->jsonSerialize(); + $jsonTask['error_message'] = $task->getErrorMessage(); + return $jsonTask; + }, $tasks); + + $this->writeArrayInOutputFormat($input, $output, $arrayTasks); + return 0; + } +} diff --git a/core/Command/TaskProcessing/Statistics.php b/core/Command/TaskProcessing/Statistics.php new file mode 100644 index 00000000000..6aa4cd5bf52 --- /dev/null +++ b/core/Command/TaskProcessing/Statistics.php @@ -0,0 +1,196 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OCP\TaskProcessing\IManager; +use OCP\TaskProcessing\Task; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Statistics extends Base { + public function __construct( + protected IManager $taskProcessingManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:stats') + ->setDescription('get statistics for tasks') + ->addOption( + 'userIdFilter', + 'u', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one user ID' + ) + ->addOption( + 'type', + 't', + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one task type' + ) + ->addOption( + 'appId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one app ID' + ) + ->addOption( + 'customId', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks for one custom ID' + ) + ->addOption( + 'status', + 's', + InputOption::VALUE_OPTIONAL, + 'only get the tasks that have a specific status (STATUS_UNKNOWN=0, STATUS_SCHEDULED=1, STATUS_RUNNING=2, STATUS_SUCCESSFUL=3, STATUS_FAILED=4, STATUS_CANCELLED=5)' + ) + ->addOption( + 'scheduledAfter', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that were scheduled after a specific date (Unix timestamp)' + ) + ->addOption( + 'endedBefore', + null, + InputOption::VALUE_OPTIONAL, + 'only get the tasks that ended before a specific date (Unix timestamp)' + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userIdFilter = $input->getOption('userIdFilter'); + if ($userIdFilter === null) { + $userIdFilter = ''; + } elseif ($userIdFilter === '') { + $userIdFilter = null; + } + $type = $input->getOption('type'); + $appId = $input->getOption('appId'); + $customId = $input->getOption('customId'); + $status = $input->getOption('status'); + $scheduledAfter = $input->getOption('scheduledAfter'); + $endedBefore = $input->getOption('endedBefore'); + + $tasks = $this->taskProcessingManager->getTasks($userIdFilter, $type, $appId, $customId, $status, $scheduledAfter, $endedBefore); + + $stats = ['Number of tasks' => count($tasks)]; + + $maxRunningTime = 0; + $totalRunningTime = 0; + $runningTimeCount = 0; + + $maxQueuingTime = 0; + $totalQueuingTime = 0; + $queuingTimeCount = 0; + + $maxUserWaitingTime = 0; + $totalUserWaitingTime = 0; + $userWaitingTimeCount = 0; + + $maxInputSize = 0; + $maxOutputSize = 0; + $inputCount = 0; + $inputSum = 0; + $outputCount = 0; + $outputSum = 0; + + foreach ($tasks as $task) { + // running time + if ($task->getStartedAt() !== null && $task->getEndedAt() !== null) { + $taskRunningTime = $task->getEndedAt() - $task->getStartedAt(); + $totalRunningTime += $taskRunningTime; + $runningTimeCount++; + if ($taskRunningTime >= $maxRunningTime) { + $maxRunningTime = $taskRunningTime; + } + } + // queuing time + if ($task->getScheduledAt() !== null && $task->getStartedAt() !== null) { + $taskQueuingTime = $task->getStartedAt() - $task->getScheduledAt(); + $totalQueuingTime += $taskQueuingTime; + $queuingTimeCount++; + if ($taskQueuingTime >= $maxQueuingTime) { + $maxQueuingTime = $taskQueuingTime; + } + } + // user waiting time + if ($task->getScheduledAt() !== null && $task->getEndedAt() !== null) { + $taskUserWaitingTime = $task->getEndedAt() - $task->getScheduledAt(); + $totalUserWaitingTime += $taskUserWaitingTime; + $userWaitingTimeCount++; + if ($taskUserWaitingTime >= $maxUserWaitingTime) { + $maxUserWaitingTime = $taskUserWaitingTime; + } + } + // input/output sizes + if ($task->getStatus() === Task::STATUS_SUCCESSFUL) { + $outputString = json_encode($task->getOutput()); + if ($outputString !== false) { + $outputCount++; + $outputLength = strlen($outputString); + $outputSum += $outputLength; + if ($outputLength > $maxOutputSize) { + $maxOutputSize = $outputLength; + } + } + } + $inputString = json_encode($task->getInput()); + if ($inputString !== false) { + $inputCount++; + $inputLength = strlen($inputString); + $inputSum += $inputLength; + if ($inputLength > $maxInputSize) { + $maxInputSize = $inputLength; + } + } + } + + if ($runningTimeCount > 0) { + $stats['Max running time'] = $maxRunningTime; + $averageRunningTime = $totalRunningTime / $runningTimeCount; + $stats['Average running time'] = (int)$averageRunningTime; + $stats['Running time count'] = $runningTimeCount; + } + if ($queuingTimeCount > 0) { + $stats['Max queuing time'] = $maxQueuingTime; + $averageQueuingTime = $totalQueuingTime / $queuingTimeCount; + $stats['Average queuing time'] = (int)$averageQueuingTime; + $stats['Queuing time count'] = $queuingTimeCount; + } + if ($userWaitingTimeCount > 0) { + $stats['Max user waiting time'] = $maxUserWaitingTime; + $averageUserWaitingTime = $totalUserWaitingTime / $userWaitingTimeCount; + $stats['Average user waiting time'] = (int)$averageUserWaitingTime; + $stats['User waiting time count'] = $userWaitingTimeCount; + } + if ($outputCount > 0) { + $stats['Max output size (bytes)'] = $maxOutputSize; + $averageOutputSize = $outputSum / $outputCount; + $stats['Average output size (bytes)'] = (int)$averageOutputSize; + $stats['Number of tasks with output'] = $outputCount; + } + if ($inputCount > 0) { + $stats['Max input size (bytes)'] = $maxInputSize; + $averageInputSize = $inputSum / $inputCount; + $stats['Average input size (bytes)'] = (int)$averageInputSize; + $stats['Number of tasks with input'] = $inputCount; + } + + $this->writeArrayInOutputFormat($input, $output, $stats); + return 0; + } +} diff --git a/core/Command/TwoFactorAuth/Base.php b/core/Command/TwoFactorAuth/Base.php index a36cb2af374..034ea36afca 100644 --- a/core/Command/TwoFactorAuth/Base.php +++ b/core/Command/TwoFactorAuth/Base.php @@ -1,27 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\TwoFactorAuth; diff --git a/core/Command/TwoFactorAuth/Cleanup.php b/core/Command/TwoFactorAuth/Cleanup.php index 1b2c6e22632..f8f116af3fd 100644 --- a/core/Command/TwoFactorAuth/Cleanup.php +++ b/core/Command/TwoFactorAuth/Cleanup.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\TwoFactorAuth; diff --git a/core/Command/TwoFactorAuth/Disable.php b/core/Command/TwoFactorAuth/Disable.php index a593993128f..c60c1245735 100644 --- a/core/Command/TwoFactorAuth/Disable.php +++ b/core/Command/TwoFactorAuth/Disable.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\TwoFactorAuth; @@ -53,14 +38,14 @@ class Disable extends Base { $providerId = $input->getArgument('provider_id'); $user = $this->userManager->get($uid); if (is_null($user)) { - $output->writeln("<error>Invalid UID</error>"); + $output->writeln('<error>Invalid UID</error>'); return 1; } if ($this->manager->tryDisableProviderFor($providerId, $user)) { $output->writeln("Two-factor provider <options=bold>$providerId</> disabled for user <options=bold>$uid</>."); return 0; } else { - $output->writeln("<error>The provider does not support this operation.</error>"); + $output->writeln('<error>The provider does not support this operation.</error>'); return 2; } } diff --git a/core/Command/TwoFactorAuth/Enable.php b/core/Command/TwoFactorAuth/Enable.php index b0d80c43a61..215cb31397e 100644 --- a/core/Command/TwoFactorAuth/Enable.php +++ b/core/Command/TwoFactorAuth/Enable.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\TwoFactorAuth; @@ -53,14 +38,14 @@ class Enable extends Base { $providerId = $input->getArgument('provider_id'); $user = $this->userManager->get($uid); if (is_null($user)) { - $output->writeln("<error>Invalid UID</error>"); + $output->writeln('<error>Invalid UID</error>'); return 1; } if ($this->manager->tryEnableProviderFor($providerId, $user)) { $output->writeln("Two-factor provider <options=bold>$providerId</> enabled for user <options=bold>$uid</>."); return 0; } else { - $output->writeln("<error>The provider does not support this operation.</error>"); + $output->writeln('<error>The provider does not support this operation.</error>'); return 2; } } diff --git a/core/Command/TwoFactorAuth/Enforce.php b/core/Command/TwoFactorAuth/Enforce.php index 57b07308afe..3315f045bc8 100644 --- a/core/Command/TwoFactorAuth/Enforce.php +++ b/core/Command/TwoFactorAuth/Enforce.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\TwoFactorAuth; diff --git a/core/Command/TwoFactorAuth/State.php b/core/Command/TwoFactorAuth/State.php index 5663056b50a..ab2e8f2aecf 100644 --- a/core/Command/TwoFactorAuth/State.php +++ b/core/Command/TwoFactorAuth/State.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\TwoFactorAuth; @@ -55,7 +37,7 @@ class State extends Base { $uid = $input->getArgument('uid'); $user = $this->userManager->get($uid); if (is_null($user)) { - $output->writeln("<error>Invalid UID</error>"); + $output->writeln('<error>Invalid UID</error>'); return 1; } @@ -69,9 +51,9 @@ class State extends Base { $output->writeln("Two-factor authentication is not enabled for user $uid"); } - $output->writeln(""); - $this->printProviders("Enabled providers", $enabled, $output); - $this->printProviders("Disabled providers", $disabled, $output); + $output->writeln(''); + $this->printProviders('Enabled providers', $enabled, $output); + $this->printProviders('Disabled providers', $disabled, $output); return 0; } @@ -98,9 +80,9 @@ class State extends Base { return; } - $output->writeln($title . ":"); + $output->writeln($title . ':'); foreach ($providers as $provider) { - $output->writeln("- " . $provider); + $output->writeln('- ' . $provider); } } } diff --git a/core/Command/Upgrade.php b/core/Command/Upgrade.php index c74b8d27049..c3d6aacc714 100644 --- a/core/Command/Upgrade.php +++ b/core/Command/Upgrade.php @@ -1,35 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Nils Wittenbrink <nilswittenbrink@web.de> - * @author Owen Winkler <a_github@midnightcircus.com> - * @author Robin Appelman <robin@icewind.nl> - * @author Sander Ruitenbeek <sander@grids.be> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Pulzer <t.pulzer@kniel.de> - * @author Valdnet <47037905+Valdnet@users.noreply.github.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command; @@ -46,6 +20,8 @@ use OC\Updater; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; +use OCP\IURLGenerator; +use OCP\Server; use OCP\Util; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; @@ -61,7 +37,8 @@ class Upgrade extends Command { public const ERROR_FAILURE = 5; public function __construct( - private IConfig $config + private IConfig $config, + private IURLGenerator $urlGenerator, ) { parent::__construct(); } @@ -80,22 +57,23 @@ class Upgrade extends Command { */ protected function execute(InputInterface $input, OutputInterface $output): int { if (Util::needUpgrade()) { - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { // Prepend each line with a little timestamp $timestampFormatter = new TimestampFormatter($this->config, $output->getFormatter()); $output->setFormatter($timestampFormatter); } $self = $this; - $updater = \OCP\Server::get(Updater::class); + $updater = Server::get(Updater::class); + $incompatibleOverwrites = $this->config->getSystemValue('app_install_overwrite', []); /** @var IEventDispatcher $dispatcher */ - $dispatcher = \OC::$server->get(IEventDispatcher::class); + $dispatcher = Server::get(IEventDispatcher::class); $progress = new ProgressBar($output); $progress->setFormat(" %message%\n %current%/%max% [%bar%] %percent:3s%%"); $listener = function (MigratorExecuteSqlEvent $event) use ($progress, $output): void { $message = $event->getSql(); - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { $output->writeln(' Executing SQL ' . $message); } else { if (strlen($message) > 60) { @@ -131,11 +109,11 @@ class Upgrade extends Command { $progress->finish(); $output->writeln(''); } elseif ($event instanceof RepairStepEvent) { - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { $output->writeln('<info>Repair step: ' . $event->getStepName() . '</info>'); } } elseif ($event instanceof RepairInfoEvent) { - if (OutputInterface::VERBOSITY_NORMAL < $output->getVerbosity()) { + if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) { $output->writeln('<info>Repair info: ' . $event->getMessage() . '</info>'); } } elseif ($event instanceof RepairWarningEvent) { @@ -155,59 +133,61 @@ class Upgrade extends Command { $dispatcher->addListener(RepairErrorEvent::class, $repairListener); - $updater->listen('\OC\Updater', 'maintenanceEnabled', function () use ($output) { + $updater->listen('\OC\Updater', 'maintenanceEnabled', function () use ($output): void { $output->writeln('<info>Turned on maintenance mode</info>'); }); - $updater->listen('\OC\Updater', 'maintenanceDisabled', function () use ($output) { + $updater->listen('\OC\Updater', 'maintenanceDisabled', function () use ($output): void { $output->writeln('<info>Turned off maintenance mode</info>'); }); - $updater->listen('\OC\Updater', 'maintenanceActive', function () use ($output) { + $updater->listen('\OC\Updater', 'maintenanceActive', function () use ($output): void { $output->writeln('<info>Maintenance mode is kept active</info>'); }); $updater->listen('\OC\Updater', 'updateEnd', - function ($success) use ($output, $self) { + function ($success) use ($output, $self): void { if ($success) { - $message = "<info>Update successful</info>"; + $message = '<info>Update successful</info>'; } else { - $message = "<error>Update failed</error>"; + $message = '<error>Update failed</error>'; } $output->writeln($message); }); - $updater->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($output) { + $updater->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($output): void { $output->writeln('<info>Updating database schema</info>'); }); - $updater->listen('\OC\Updater', 'dbUpgrade', function () use ($output) { + $updater->listen('\OC\Updater', 'dbUpgrade', function () use ($output): void { $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): void { + if (!in_array($app, $incompatibleOverwrites)) { + $output->writeln('<comment>Disabled incompatible app: ' . $app . '</comment>'); + } }); - $updater->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($output) { + $updater->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($output): void { $output->writeln('<info>Update app ' . $app . ' from App Store</info>'); }); - $updater->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($output) { + $updater->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($output): void { $output->writeln("<info>Checking whether the database schema for <$app> can be updated (this can take a long time depending on the database size)</info>"); }); - $updater->listen('\OC\Updater', 'appUpgradeStarted', function ($app, $version) use ($output) { + $updater->listen('\OC\Updater', 'appUpgradeStarted', function ($app, $version) use ($output): void { $output->writeln("<info>Updating <$app> ...</info>"); }); - $updater->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($output) { + $updater->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($output): void { $output->writeln("<info>Updated <$app> to $version</info>"); }); - $updater->listen('\OC\Updater', 'failure', function ($message) use ($output, $self) { + $updater->listen('\OC\Updater', 'failure', function ($message) use ($output, $self): void { $output->writeln("<error>$message</error>"); }); - $updater->listen('\OC\Updater', 'setDebugLogLevel', function ($logLevel, $logLevelName) use ($output) { - $output->writeln("<info>Setting log level to debug</info>"); + $updater->listen('\OC\Updater', 'setDebugLogLevel', function ($logLevel, $logLevelName) use ($output): void { + $output->writeln('<info>Setting log level to debug</info>'); }); - $updater->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($output) { - $output->writeln("<info>Resetting log level</info>"); + $updater->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($output): void { + $output->writeln('<info>Resetting log level</info>'); }); - $updater->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($output) { - $output->writeln("<info>Starting code integrity check...</info>"); + $updater->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($output): void { + $output->writeln('<info>Starting code integrity check...</info>'); }); - $updater->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($output) { - $output->writeln("<info>Finished code integrity check</info>"); + $updater->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($output): void { + $output->writeln('<info>Finished code integrity check</info>'); }); $success = $updater->upgrade(); @@ -228,7 +208,11 @@ class Upgrade extends Command { . 'config.php and call this script again.</comment>', true); return self::ERROR_MAINTENANCE_MODE; } else { - $output->writeln('<info>Nextcloud is already latest version</info>'); + $output->writeln('<info>No upgrade required.</info>'); + $output->writeln(''); + $output->writeln('Note: This command triggers the upgrade actions associated with a new version. The new version\'s updated source files must be deployed in advance.'); + $doc = $this->urlGenerator->linkToDocs('admin-update'); + $output->writeln('See the upgrade documentation: ' . $doc . ' for more information.'); return self::ERROR_UP_TO_DATE; } } @@ -243,9 +227,9 @@ class Upgrade extends Command { $trustedDomains = $this->config->getSystemValue('trusted_domains', []); if (empty($trustedDomains)) { $output->write( - '<warning>The setting "trusted_domains" could not be ' . - 'set automatically by the upgrade script, ' . - 'please set it manually</warning>' + '<warning>The setting "trusted_domains" could not be ' + . 'set automatically by the upgrade script, ' + . 'please set it manually</warning>' ); } } diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php index 5e8ab92070d..4de4e247991 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -1,35 +1,23 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Laurens Post <lkpost@scept.re> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; use OC\Files\Filesystem; +use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Security\Events\GenerateSecurePasswordEvent; +use OCP\Security\ISecureRandom; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; @@ -42,52 +30,77 @@ class Add extends Command { 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(); } - protected function configure() { + protected function configure(): void { $this ->setName('user:add') - ->setDescription('adds a user') + ->setDescription('adds an account') ->addArgument( 'uid', InputArgument::REQUIRED, - 'User ID used to login (must only contain a-z, A-Z, 0-9, -, _ and @)' + 'Account ID used to login (must only contain a-z, A-Z, 0-9, -, _ and @)' ) ->addOption( 'password-from-env', null, InputOption::VALUE_NONE, - 'read password from environment variable OC_PASS' + 'read password from environment variable NC_PASS/OC_PASS' + ) + ->addOption( + 'generate-password', + null, + InputOption::VALUE_NONE, + 'Generate a secure password. A welcome email with a reset link will be sent to the user via an email if --email option and newUser.sendEmail config are set' ) ->addOption( 'display-name', null, InputOption::VALUE_OPTIONAL, - 'User name used in the web UI (can contain any characters)' + 'Login used in the web UI (can contain any characters)' ) ->addOption( 'group', 'g', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, - 'groups the user should be added to (The group will be created if it does not exist)' + 'groups the account should be added to (The group will be created if it does not exist)' + ) + ->addOption( + 'email', + null, + InputOption::VALUE_REQUIRED, + 'When set, users may register using the default email verification workflow' ); } protected function execute(InputInterface $input, OutputInterface $output): int { $uid = $input->getArgument('uid'); if ($this->userManager->userExists($uid)) { - $output->writeln('<error>The user "' . $uid . '" already exists.</error>'); + $output->writeln('<error>The account "' . $uid . '" already exists.</error>'); return 1; } + $password = ''; + + // Setup password. if ($input->getOption('password-from-env')) { - $password = getenv('OC_PASS'); + $password = getenv('NC_PASS') ?: getenv('OC_PASS'); + if (!$password) { - $output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>'); + $output->writeln('<error>--password-from-env given, but NC_PASS/OC_PASS is empty!</error>'); return 1; } + } elseif ($input->getOption('generate-password')) { + $passwordEvent = new GenerateSecurePasswordEvent(); + $this->eventDispatcher->dispatchTyped($passwordEvent); + $password = $passwordEvent->getPassword() ?? $this->secureRandom->generate(20); } elseif ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); @@ -101,29 +114,28 @@ class Add extends Command { $confirm = $helper->ask($input, $output, $question); if ($password !== $confirm) { - $output->writeln("<error>Passwords did not match!</error>"); + $output->writeln('<error>Passwords did not match!</error>'); return 1; } } else { - $output->writeln("<error>Interactive input or --password-from-env is needed for entering a password!</error>"); + $output->writeln('<error>Interactive input or --password-from-env or --generate-password is needed for setting a password!</error>'); return 1; } try { $user = $this->userManager->createUser( $input->getArgument('uid'), - $password + $password, ); } catch (\Exception $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); return 1; } - if ($user instanceof IUser) { - $output->writeln('<info>The user "' . $user->getUID() . '" was created successfully</info>'); + $output->writeln('<info>The account "' . $user->getUID() . '" was created successfully</info>'); } else { - $output->writeln('<error>An error occurred while creating the user</error>'); + $output->writeln('<error>An error occurred while creating the account</error>'); return 1; } @@ -151,9 +163,33 @@ class Add extends Command { } if ($group instanceof IGroup) { $group->addUser($user); - $output->writeln('User "' . $user->getUID() . '" added to group "' . $group->getGID() . '"'); + $output->writeln('Account "' . $user->getUID() . '" added to group "' . $group->getGID() . '"'); } } + + $email = $input->getOption('email'); + if (!empty($email)) { + if (!$this->mailer->validateMailAddress($email)) { + $output->writeln(\sprintf( + '<error>The given email address "%s" is invalid. Email not set for the user.</error>', + $email, + )); + + return 1; + } + + $user->setSystemEMailAddress($email); + + if ($this->appConfig->getValueString('core', 'newUser.sendEmail', 'yes') === 'yes') { + try { + $this->mailHelper->sendMail($user, $this->mailHelper->generateTemplate($user, true)); + $output->writeln('Welcome email sent to ' . $email); + } catch (\Exception $e) { + $output->writeln('Unable to send the welcome email to ' . $email); + } + } + } + return 0; } } diff --git a/core/Command/User/AuthTokens/Add.php b/core/Command/User/AuthTokens/Add.php index e067c069c79..89b20535c63 100644 --- a/core/Command/User/AuthTokens/Add.php +++ b/core/Command/User/AuthTokens/Add.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, NextCloud, Inc. - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Sean Molenaar <sean@seanmolenaar.eu> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\User\AuthTokens; @@ -54,11 +36,11 @@ class Add extends Command { $this ->setName('user:auth-tokens:add') ->setAliases(['user:add-app-password']) - ->setDescription('Add app password for the named user') + ->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', @@ -75,21 +57,21 @@ class Add extends Command { $user = $this->userManager->get($username); if (is_null($user)) { - $output->writeln('<error>User does not exist</error>'); + $output->writeln('<error>Account does not exist</error>'); return 1; } if ($input->getOption('password-from-env')) { - $password = getenv('NC_PASS') ?? getenv('OC_PASS'); + $password = getenv('NC_PASS') ?: getenv('OC_PASS'); if (!$password) { - $output->writeln('<error>--password-from-env given, but NC_PASS is empty!</error>'); + $output->writeln('<error>--password-from-env given, but NC_PASS/OC_PASS is empty!</error>'); return 1; } } elseif ($input->isInteractive()) { /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); - $question = new Question('Enter the user password: '); + $question = new Question('Enter the account password: '); $question->setHidden(true); /** @var null|string $password */ $password = $helper->ask($input, $output, $question); @@ -99,7 +81,7 @@ class Add extends Command { $output->writeln('<info>No password provided. The generated app password will therefore have limited capabilities. Any operation that requires the login password will fail.</info>'); } - $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS); + $token = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); $generatedToken = $this->tokenProvider->generateToken( $token, $user->getUID(), diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php index 56bfcf787f8..2047d2eae2a 100644 --- a/core/Command/User/AuthTokens/Delete.php +++ b/core/Command/User/AuthTokens/Delete.php @@ -1,24 +1,8 @@ <?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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\User\AuthTokens; @@ -63,7 +47,7 @@ class Delete extends Base { protected function execute(InputInterface $input, OutputInterface $output): int { $uid = $input->getArgument('uid'); - $id = (int) $input->getArgument('id'); + $id = (int)$input->getArgument('id'); $before = $input->getOption('last-used-before'); if ($before) { diff --git a/core/Command/User/AuthTokens/ListCommand.php b/core/Command/User/AuthTokens/ListCommand.php index 0bcd2e55225..b36aa717505 100644 --- a/core/Command/User/AuthTokens/ListCommand.php +++ b/core/Command/User/AuthTokens/ListCommand.php @@ -1,24 +1,8 @@ <?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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\User\AuthTokens; diff --git a/core/Command/User/ClearGeneratedAvatarCacheCommand.php b/core/Command/User/ClearGeneratedAvatarCacheCommand.php new file mode 100644 index 00000000000..515b3a913b7 --- /dev/null +++ b/core/Command/User/ClearGeneratedAvatarCacheCommand.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\User; + +use OC\Avatar\AvatarManager; +use OC\Core\Command\Base; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ClearGeneratedAvatarCacheCommand extends Base { + public function __construct( + protected AvatarManager $avatarManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setDescription('clear avatar cache') + ->setName('user:clear-avatar-cache'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln('Clearing avatar cache has started'); + $this->avatarManager->clearCachedAvatars(); + $output->writeln('Cleared avatar cache successfully'); + return 0; + } +} diff --git a/core/Command/User/Delete.php b/core/Command/User/Delete.php index 94390a3f070..c5d0578f5f8 100644 --- a/core/Command/User/Delete.php +++ b/core/Command/User/Delete.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Jens-Christian Fischer <jens-christian.fischer@switch.ch> - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; diff --git a/core/Command/User/Disable.php b/core/Command/User/Disable.php index 3fdf6220f45..4713950bf30 100644 --- a/core/Command/User/Disable.php +++ b/core/Command/User/Disable.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; diff --git a/core/Command/User/Enable.php b/core/Command/User/Enable.php index 2055bd30cec..23f56e5dd4f 100644 --- a/core/Command/User/Enable.php +++ b/core/Command/User/Enable.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; diff --git a/core/Command/User/Info.php b/core/Command/User/Info.php index a8fb62099e2..e7fc9286e74 100644 --- a/core/Command/User/Info.php +++ b/core/Command/User/Info.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\User; use OC\Core\Command\Base; +use OCP\Files\NotFoundException; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; @@ -75,7 +58,8 @@ class Info extends Base { 'groups' => $groups, 'quota' => $user->getQuota(), 'storage' => $this->getStorageInfo($user), - 'last_seen' => date(\DateTimeInterface::ATOM, $user->getLastLogin()), // ISO-8601 + 'first_seen' => $this->formatLoginDate($user->getFirstLogin()), + 'last_seen' => $this->formatLoginDate($user->getLastLogin()), 'user_directory' => $user->getHome(), 'backend' => $user->getBackendClassName() ]; @@ -83,6 +67,16 @@ class Info extends Base { return 0; } + private function formatLoginDate(int $timestamp): string { + if ($timestamp < 0) { + return 'unknown'; + } elseif ($timestamp === 0) { + return 'never'; + } else { + return date(\DateTimeInterface::ATOM, $timestamp); // ISO-8601 + } + } + /** * @param IUser $user * @return array @@ -92,7 +86,7 @@ class Info extends Base { \OC_Util::setupFS($user->getUID()); try { $storage = \OC_Helper::getStorageInfo('/'); - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { return []; } return [ diff --git a/core/Command/User/Keys/Verify.php b/core/Command/User/Keys/Verify.php new file mode 100644 index 00000000000..024e9346072 --- /dev/null +++ b/core/Command/User/Keys/Verify.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\User\Keys; + +use OC\Security\IdentityProof\Manager; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Verify extends Command { + public function __construct( + protected IUserManager $userManager, + protected Manager $keyManager, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('user:keys:verify') + ->setDescription('Verify if the stored public key matches the stored private key') + ->addArgument( + 'user-id', + InputArgument::REQUIRED, + 'User ID of the user to verify' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('user-id'); + + $user = $this->userManager->get($userId); + if (!$user instanceof IUser) { + $output->writeln('Unknown user'); + return static::FAILURE; + } + + $key = $this->keyManager->getKey($user); + $publicKey = $key->getPublic(); + $privateKey = $key->getPrivate(); + + $output->writeln('User public key size: ' . strlen($publicKey)); + $output->writeln('User private key size: ' . strlen($privateKey)); + + // Derive the public key from the private key again to validate the stored public key + $opensslPrivateKey = openssl_pkey_get_private($privateKey); + $publicKeyDerived = openssl_pkey_get_details($opensslPrivateKey); + $publicKeyDerived = $publicKeyDerived['key']; + $output->writeln('User derived public key size: ' . strlen($publicKeyDerived)); + + $output->writeln(''); + + $output->writeln('Stored public key:'); + $output->writeln($publicKey); + $output->writeln('Derived public key:'); + $output->writeln($publicKeyDerived); + + if ($publicKey != $publicKeyDerived) { + $output->writeln('<error>Stored public key does not match stored private key</error>'); + return static::FAILURE; + } + + $output->writeln('<info>Stored public key matches stored private key</info>'); + + return static::SUCCESS; + } +} diff --git a/core/Command/User/LastSeen.php b/core/Command/User/LastSeen.php index 90ddb6992d9..984def72cd6 100644 --- a/core/Command/User/LastSeen.php +++ b/core/Command/User/LastSeen.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Pierre Ozoux <pierre@ozoux.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; @@ -31,6 +13,7 @@ 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 { @@ -40,34 +23,61 @@ class LastSeen extends Base { parent::__construct(); } - protected function configure() { + protected function configure(): void { $this ->setName('user:lastseen') ->setDescription('shows when the user was logged in last time') ->addArgument( 'uid', - InputArgument::REQUIRED, + InputArgument::OPTIONAL, 'the username' - ); + ) + ->addOption( + 'all', + null, + InputOption::VALUE_NONE, + 'shows a list of when all users were last logged in' + ) + ; } protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $this->userManager->get($input->getArgument('uid')); - if (is_null($user)) { - $output->writeln('<error>User does not exist</error>'); - return 1; + $singleUserId = $input->getArgument('uid'); + + if ($singleUserId) { + $user = $this->userManager->get($singleUserId); + if (is_null($user)) { + $output->writeln('<error>User does not exist</error>'); + return 1; + } + + $lastLogin = $user->getLastLogin(); + if ($lastLogin === 0) { + $output->writeln($user->getUID() . ' has never logged in.'); + } else { + $date = new \DateTime(); + $date->setTimestamp($lastLogin); + $output->writeln($user->getUID() . "'s last login: " . $date->format('Y-m-d H:i:s T')); + } + + return 0; } - $lastLogin = $user->getLastLogin(); - if ($lastLogin === 0) { - $output->writeln('User ' . $user->getUID() . - ' has never logged in, yet.'); - } else { - $date = new \DateTime(); - $date->setTimestamp($lastLogin); - $output->writeln($user->getUID() . - '`s last login: ' . $date->format('d.m.Y H:i')); + if (!$input->getOption('all')) { + $output->writeln('<error>Please specify a username, or "--all" to list all</error>'); + return 1; } + + $this->userManager->callForAllUsers(static function (IUser $user) use ($output): void { + $lastLogin = $user->getLastLogin(); + if ($lastLogin === 0) { + $output->writeln($user->getUID() . ' has never logged in.'); + } else { + $date = new \DateTime(); + $date->setTimestamp($lastLogin); + $output->writeln($user->getUID() . "'s last login: " . $date->format('Y-m-d H:i:s T')); + } + }); return 0; } diff --git a/core/Command/User/ListCommand.php b/core/Command/User/ListCommand.php index f25d6c2dae9..66b831c793b 100644 --- a/core/Command/User/ListCommand.php +++ b/core/Command/User/ListCommand.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\User; @@ -45,6 +27,11 @@ class ListCommand extends Base { ->setName('user:list') ->setDescription('list configured users') ->addOption( + 'disabled', + 'd', + InputOption::VALUE_NONE, + 'List disabled users only' + )->addOption( 'limit', 'l', InputOption::VALUE_OPTIONAL, @@ -71,7 +58,11 @@ class ListCommand extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $users = $this->userManager->searchDisplayName('', (int) $input->getOption('limit'), (int) $input->getOption('offset')); + if ($input->getOption('disabled')) { + $users = $this->userManager->getDisabledUsers((int)$input->getOption('limit'), (int)$input->getOption('offset')); + } else { + $users = $this->userManager->searchDisplayName('', (int)$input->getOption('limit'), (int)$input->getOption('offset')); + } $this->writeArrayInOutputFormat($input, $output, $this->formatUsers($users, (bool)$input->getOption('info'))); return 0; @@ -79,18 +70,13 @@ class ListCommand extends Base { /** * @param IUser[] $users - * @param bool [$detailed=false] - * @return array + * @return \Generator<string,string|array> */ - private function formatUsers(array $users, bool $detailed = false) { - $keys = array_map(function (IUser $user) { - return $user->getUID(); - }, $users); - - $values = array_map(function (IUser $user) use ($detailed) { + private function formatUsers(array $users, bool $detailed = false): \Generator { + foreach ($users as $user) { if ($detailed) { $groups = $this->groupManager->getUserGroupIds($user); - return [ + $value = [ 'user_id' => $user->getUID(), 'display_name' => $user->getDisplayName(), 'email' => (string)$user->getSystemEMailAddress(), @@ -98,13 +84,25 @@ class ListCommand extends Base { 'enabled' => $user->isEnabled(), 'groups' => $groups, 'quota' => $user->getQuota(), - 'last_seen' => date(\DateTimeInterface::ATOM, $user->getLastLogin()), // ISO-8601 + 'first_seen' => $this->formatLoginDate($user->getFirstLogin()), + 'last_seen' => $this->formatLoginDate($user->getLastLogin()), 'user_directory' => $user->getHome(), 'backend' => $user->getBackendClassName() ]; + } else { + $value = $user->getDisplayName(); } - return $user->getDisplayName(); - }, $users); - return array_combine($keys, $values); + yield $user->getUID() => $value; + } + } + + private function formatLoginDate(int $timestamp): string { + if ($timestamp < 0) { + return 'unknown'; + } elseif ($timestamp === 0) { + return 'never'; + } else { + return date(\DateTimeInterface::ATOM, $timestamp); // ISO-8601 + } } } diff --git a/core/Command/User/Profile.php b/core/Command/User/Profile.php new file mode 100644 index 00000000000..fd5fbed08cd --- /dev/null +++ b/core/Command/User/Profile.php @@ -0,0 +1,234 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\User; + +use OC\Core\Command\Base; +use OCP\Accounts\IAccount; +use OCP\Accounts\IAccountManager; +use OCP\Accounts\PropertyDoesNotExistException; +use OCP\IUser; +use OCP\IUserManager; +use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Profile extends Base { + public function __construct( + protected IUserManager $userManager, + protected IAccountManager $accountManager, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + $this + ->setName('user:profile') + ->setDescription('Read and modify user profile properties') + ->addArgument( + 'uid', + InputArgument::REQUIRED, + 'Account ID used to login' + ) + ->addArgument( + 'key', + InputArgument::OPTIONAL, + 'Profile property to set, get or delete', + '' + ) + + // Get + ->addOption( + 'default-value', + null, + InputOption::VALUE_REQUIRED, + '(Only applicable on get) If no default value is set and the property does not exist, the command will exit with 1' + ) + + // Set + ->addArgument( + 'value', + InputArgument::OPTIONAL, + 'The new value of the property', + null + ) + ->addOption( + 'update-only', + null, + InputOption::VALUE_NONE, + 'Only updates the value, if it is not set before, it is not being added' + ) + + // Delete + ->addOption( + 'delete', + null, + InputOption::VALUE_NONE, + 'Specify this option to delete the property value' + ) + ->addOption( + 'error-if-not-exists', + null, + InputOption::VALUE_NONE, + 'Checks whether the property exists before deleting it' + ) + ; + } + + protected function checkInput(InputInterface $input): IUser { + $uid = $input->getArgument('uid'); + $user = $this->userManager->get($uid); + if (!$user) { + throw new \InvalidArgumentException('The user "' . $uid . '" does not exist.'); + } + // normalize uid + $input->setArgument('uid', $user->getUID()); + + $key = $input->getArgument('key'); + if ($key === '') { + if ($input->hasParameterOption('--default-value')) { + throw new \InvalidArgumentException('The "default-value" option can only be used when specifying a key.'); + } + if ($input->getArgument('value') !== null) { + throw new \InvalidArgumentException('The value argument can only be used when specifying a key.'); + } + if ($input->getOption('delete')) { + throw new \InvalidArgumentException('The "delete" option can only be used when specifying a key.'); + } + } + + if ($input->getArgument('value') !== null && $input->hasParameterOption('--default-value')) { + throw new \InvalidArgumentException('The value argument can not be used together with "default-value".'); + } + if ($input->getOption('update-only') && $input->getArgument('value') === null) { + throw new \InvalidArgumentException('The "update-only" option can only be used together with "value".'); + } + + if ($input->getOption('delete') && $input->hasParameterOption('--default-value')) { + throw new \InvalidArgumentException('The "delete" option can not be used together with "default-value".'); + } + if ($input->getOption('delete') && $input->getArgument('value') !== null) { + throw new \InvalidArgumentException('The "delete" option can not be used together with "value".'); + } + if ($input->getOption('error-if-not-exists') && !$input->getOption('delete')) { + throw new \InvalidArgumentException('The "error-if-not-exists" option can only be used together with "delete".'); + } + + return $user; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + try { + $user = $this->checkInput($input); + } catch (\InvalidArgumentException $e) { + $output->writeln('<error>' . $e->getMessage() . '</error>'); + return self::FAILURE; + } + + $uid = $input->getArgument('uid'); + $key = $input->getArgument('key'); + $userAccount = $this->accountManager->getAccount($user); + + if ($key === '') { + $settings = $this->getAllProfileProperties($userAccount); + $this->writeArrayInOutputFormat($input, $output, $settings); + return self::SUCCESS; + } + + $value = $this->getStoredValue($userAccount, $key); + $inputValue = $input->getArgument('value'); + if ($inputValue !== null) { + if ($input->hasParameterOption('--update-only') && $value === null) { + $output->writeln('<error>The property does not exist for user "' . $uid . '".</error>'); + return self::FAILURE; + } + + return $this->editProfileProperty($output, $userAccount, $key, $inputValue); + } elseif ($input->hasParameterOption('--delete')) { + if ($input->hasParameterOption('--error-if-not-exists') && $value === null) { + $output->writeln('<error>The property does not exist for user "' . $uid . '".</error>'); + return self::FAILURE; + } + + return $this->deleteProfileProperty($output, $userAccount, $key); + } elseif ($value !== null) { + $output->writeln($value); + } elseif ($input->hasParameterOption('--default-value')) { + $output->writeln($input->getOption('default-value')); + } else { + $output->writeln('<error>The property does not exist for user "' . $uid . '".</error>'); + return self::FAILURE; + } + + return self::SUCCESS; + } + + private function deleteProfileProperty(OutputInterface $output, IAccount $userAccount, string $key): int { + return $this->editProfileProperty($output, $userAccount, $key, ''); + } + + private function editProfileProperty(OutputInterface $output, IAccount $userAccount, string $key, string $value): int { + try { + $userAccount->getProperty($key)->setValue($value); + } catch (PropertyDoesNotExistException $exception) { + $output->writeln('<error>' . $exception->getMessage() . '</error>'); + return self::FAILURE; + } + + $this->accountManager->updateAccount($userAccount); + return self::SUCCESS; + } + + private function getStoredValue(IAccount $userAccount, string $key): ?string { + try { + $property = $userAccount->getProperty($key); + } catch (PropertyDoesNotExistException) { + return null; + } + return $property->getValue() === '' ? null : $property->getValue(); + } + + private function getAllProfileProperties(IAccount $userAccount): array { + $properties = []; + + foreach ($userAccount->getAllProperties() as $property) { + if ($property->getValue() !== '') { + $properties[$property->getName()] = $property->getValue(); + } + } + + return $properties; + } + + /** + * @param string $argumentName + * @param CompletionContext $context + * @return string[] + */ + public function completeArgumentValues($argumentName, CompletionContext $context): array { + if ($argumentName === 'uid') { + return array_map(static fn (IUser $user) => $user->getUID(), $this->userManager->search($context->getCurrentWord())); + } + if ($argumentName === 'key') { + $userId = $context->getWordAtIndex($context->getWordIndex() - 1); + $user = $this->userManager->get($userId); + if (!($user instanceof IUser)) { + return []; + } + + $account = $this->accountManager->getAccount($user); + + $properties = $this->getAllProfileProperties($account); + return array_keys($properties); + } + return []; + } +} diff --git a/core/Command/User/Report.php b/core/Command/User/Report.php index 74ab5c29cc1..c0f054adb00 100644 --- a/core/Command/User/Report.php +++ b/core/Command/User/Report.php @@ -3,29 +3,9 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; @@ -63,7 +43,7 @@ class Report extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { $table = new Table($output); - $table->setHeaders(['User Report', '']); + $table->setHeaders(['Account Report', '']); $userCountArray = $this->countUsers(); $total = 0; if (!empty($userCountArray)) { diff --git a/core/Command/User/ResetPassword.php b/core/Command/User/ResetPassword.php index 6719f893be3..0e8b1325770 100644 --- a/core/Command/User/ResetPassword.php +++ b/core/Command/User/ResetPassword.php @@ -1,29 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Clark Tomlinson <fallen013@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Laurens Post <lkpost@scept.re> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sujith H <sharidasan@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; @@ -55,13 +35,13 @@ class ResetPassword extends Base { ->addArgument( 'user', InputArgument::REQUIRED, - 'Username to reset password' + 'Login to reset password' ) ->addOption( 'password-from-env', null, InputOption::VALUE_NONE, - 'read password from environment variable OC_PASS' + 'read password from environment variable NC_PASS/OC_PASS' ) ; } @@ -76,9 +56,9 @@ class ResetPassword extends Base { } if ($input->getOption('password-from-env')) { - $password = getenv('OC_PASS'); + $password = getenv('NC_PASS') ?: getenv('OC_PASS'); if (!$password) { - $output->writeln('<error>--password-from-env given, but OC_PASS is empty!</error>'); + $output->writeln('<error>--password-from-env given, but NC_PASS/OC_PASS is empty!</error>'); return 1; } } elseif ($input->isInteractive()) { @@ -101,7 +81,7 @@ class ResetPassword extends Base { $password = $helper->ask($input, $output, $question); if ($password === null) { - $output->writeln("<error>Password cannot be empty!</error>"); + $output->writeln('<error>Password cannot be empty!</error>'); return 1; } @@ -110,11 +90,11 @@ class ResetPassword extends Base { $confirm = $helper->ask($input, $output, $question); if ($password !== $confirm) { - $output->writeln("<error>Passwords did not match!</error>"); + $output->writeln('<error>Passwords did not match!</error>'); return 1; } } else { - $output->writeln("<error>Interactive input or --password-from-env is needed for entering a new password!</error>"); + $output->writeln('<error>Interactive input or --password-from-env is needed for entering a new password!</error>'); return 1; } @@ -127,9 +107,9 @@ class ResetPassword extends Base { } if ($success) { - $output->writeln("<info>Successfully reset password for " . $username . "</info>"); + $output->writeln('<info>Successfully reset password for ' . $username . '</info>'); } else { - $output->writeln("<error>Error while resetting password!</error>"); + $output->writeln('<error>Error while resetting password!</error>'); return 1; } return 0; diff --git a/core/Command/User/Setting.php b/core/Command/User/Setting.php index 8a8736fdaa2..7fc5aab1dc7 100644 --- a/core/Command/User/Setting.php +++ b/core/Command/User/Setting.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Leuker <j.leuker@hosting.de> - * @author Kim Brose <kim.brose@rwth-aachen.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Core\Command\User; @@ -51,7 +33,7 @@ class Setting extends Base { ->addArgument( 'uid', InputArgument::REQUIRED, - 'User ID used to login' + 'Account ID used to login' ) ->addArgument( 'app', @@ -173,7 +155,8 @@ class Setting extends Base { $user = $this->userManager->get($uid); if ($user instanceof IUser) { if ($key === 'email') { - $user->setEMailAddress($input->getArgument('value')); + $email = $input->getArgument('value'); + $user->setSystemEMailAddress(mb_strtolower(trim($email))); } elseif ($key === 'display_name') { if (!$user->setDisplayName($input->getArgument('value'))) { if ($user->getDisplayName() === $input->getArgument('value')) { @@ -237,7 +220,7 @@ class Setting extends Base { } } - protected function getUserSettings($uid, $app) { + protected function getUserSettings(string $uid, string $app): array { $settings = $this->config->getAllUserValues($uid); if ($app !== '') { if (isset($settings[$app])) { @@ -248,7 +231,10 @@ class Setting extends Base { } $user = $this->userManager->get($uid); - $settings['settings']['display_name'] = $user->getDisplayName(); + if ($user !== null) { + // Only add the display name if the user exists + $settings['settings']['display_name'] = $user->getDisplayName(); + } return $settings; } diff --git a/core/Command/User/SyncAccountDataCommand.php b/core/Command/User/SyncAccountDataCommand.php index 6a4a600ea03..c353df6fe9f 100644 --- a/core/Command/User/SyncAccountDataCommand.php +++ b/core/Command/User/SyncAccountDataCommand.php @@ -1,24 +1,8 @@ <?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/>. - * + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\Core\Command\User; @@ -33,15 +17,10 @@ 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 + protected IUserManager $userManager, + protected IAccountManager $accountManager, ) { - $this->userManager = $userManager; - $this->accountManager = $accountManager; parent::__construct(); } @@ -65,7 +44,7 @@ class SyncAccountDataCommand extends Base { } protected function execute(InputInterface $input, OutputInterface $output): int { - $users = $this->userManager->searchDisplayName('', (int) $input->getOption('limit'), (int) $input->getOption('offset')); + $users = $this->userManager->searchDisplayName('', (int)$input->getOption('limit'), (int)$input->getOption('offset')); foreach ($users as $user) { $this->updateUserAccount($user, $output); diff --git a/core/Command/User/Welcome.php b/core/Command/User/Welcome.php new file mode 100644 index 00000000000..65637759689 --- /dev/null +++ b/core/Command/User/Welcome.php @@ -0,0 +1,78 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2023 FedericoHeichou <federicoheichou@gmail.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\Command\User; + +use OC\Core\Command\Base; +use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class Welcome extends Base { + /** + * @param IUserManager $userManager + * @param NewUserMailHelper $newUserMailHelper + */ + public function __construct( + protected IUserManager $userManager, + private NewUserMailHelper $newUserMailHelper, + ) { + parent::__construct(); + } + + /** + * @return void + */ + protected function configure() { + $this + ->setName('user:welcome') + ->setDescription('Sends the welcome email') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'The user to send the email to' + ) + ->addOption( + 'reset-password', + 'r', + InputOption::VALUE_NONE, + 'Add the reset password link to the email' + ) + ; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $userId = $input->getArgument('user'); + // check if user exists + $user = $this->userManager->get($userId); + if ($user === null) { + $output->writeln('<error>User does not exist</error>'); + return 1; + } + $email = $user->getEMailAddress(); + if ($email === '' || $email === null) { + $output->writeln('<error>User does not have an email address</error>'); + return 1; + } + try { + $emailTemplate = $this->newUserMailHelper->generateTemplate($user, $input->getOption('reset-password')); + $this->newUserMailHelper->sendMail($user, $emailTemplate); + } catch (\Exception $e) { + $output->writeln('<error>Failed to send email: ' . $e->getMessage() . '</error>'); + return 1; + } + return 0; + } +} |