diff options
Diffstat (limited to 'core/Command')
64 files changed, 1126 insertions, 218 deletions
diff --git a/core/Command/App/Disable.php b/core/Command/App/Disable.php index a0a20ef21dd..121ad3f010c 100644 --- a/core/Command/App/Disable.php +++ b/core/Command/App/Disable.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only diff --git a/core/Command/App/ListApps.php b/core/Command/App/ListApps.php index bb59e441119..dc947bea55f 100644 --- a/core/Command/App/ListApps.php +++ b/core/Command/App/ListApps.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only diff --git a/core/Command/App/Update.php b/core/Command/App/Update.php index b2d02e222de..71c7f84e5b0 100644 --- a/core/Command/App/Update.php +++ b/core/Command/App/Update.php @@ -9,6 +9,7 @@ declare(strict_types=1); 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; @@ -64,7 +65,7 @@ class Update extends Command { $apps = [$singleAppId]; try { $this->manager->getAppPath($singleAppId); - } catch (\OCP\App\AppPathNotFoundException $e) { + } catch (AppPathNotFoundException $e) { $output->writeln($singleAppId . ' not installed'); return 1; } diff --git a/core/Command/Background/Job.php b/core/Command/Background/Job.php index 7fa005cf231..9a862f5a13a 100644 --- a/core/Command/Background/Job.php +++ b/core/Command/Background/Job.php @@ -10,6 +10,8 @@ namespace OC\Core\Command\Background; use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; +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; @@ -75,7 +77,7 @@ class Job extends Command { $output->writeln('<info>Job executed!</info>'); $output->writeln(''); - if ($job instanceof \OCP\BackgroundJob\TimedJob) { + if ($job instanceof TimedJob) { $this->printJobInfo($jobId, $job, $output); } } else { @@ -99,10 +101,10 @@ class Job extends Command { $output->writeln('Job class: ' . get_class($job)); $output->writeln('Arguments: ' . json_encode($job->getArgument())); - $isTimedJob = $job instanceof \OCP\BackgroundJob\TimedJob; + $isTimedJob = $job instanceof TimedJob; if ($isTimedJob) { $output->writeln('Type: timed'); - } elseif ($job instanceof \OCP\BackgroundJob\QueuedJob) { + } elseif ($job instanceof QueuedJob) { $output->writeln('Type: queued'); } else { $output->writeln('Type: job'); diff --git a/core/Command/Background/JobBase.php b/core/Command/Background/JobBase.php index d92bb77d4b6..81d16f874eb 100644 --- a/core/Command/Background/JobBase.php +++ b/core/Command/Background/JobBase.php @@ -10,12 +10,15 @@ declare(strict_types=1); 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 \OC\Core\Command\Base { +abstract class JobBase extends Base { public function __construct( protected IJobList $jobList, @@ -41,10 +44,10 @@ abstract class JobBase extends \OC\Core\Command\Base { $output->writeln('Job class: ' . get_class($job)); $output->writeln('Arguments: ' . json_encode($job->getArgument())); - $isTimedJob = $job instanceof \OCP\BackgroundJob\TimedJob; + $isTimedJob = $job instanceof TimedJob; if ($isTimedJob) { $output->writeln('Type: timed'); - } elseif ($job instanceof \OCP\BackgroundJob\QueuedJob) { + } elseif ($job instanceof QueuedJob) { $output->writeln('Type: queued'); } else { $output->writeln('Type: job'); diff --git a/core/Command/Broadcast/Test.php b/core/Command/Broadcast/Test.php index fb80ce26ff0..eb8b49bc3ee 100644 --- a/core/Command/Broadcast/Test.php +++ b/core/Command/Broadcast/Test.php @@ -44,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/Config/App/Base.php b/core/Command/Config/App/Base.php index 07341c4faf9..e90a8e78f5b 100644 --- a/core/Command/Config/App/Base.php +++ b/core/Command/Config/App/Base.php @@ -7,12 +7,14 @@ declare(strict_types=1); */ namespace OC\Core\Command\Config\App; +use OC\Config\ConfigManager; use OCP\IAppConfig; use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext; abstract class Base extends \OC\Core\Command\Base { public function __construct( protected IAppConfig $appConfig, + protected readonly ConfigManager $configManager, ) { parent::__construct(); } diff --git a/core/Command/Config/App/SetConfig.php b/core/Command/Config/App/SetConfig.php index 345067cfd45..1f4ab81bf05 100644 --- a/core/Command/Config/App/SetConfig.php +++ b/core/Command/Config/App/SetConfig.php @@ -9,7 +9,6 @@ declare(strict_types=1); 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; @@ -161,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); @@ -172,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 (in_array(strtolower($value), ['true', '1', 'on', 'yes'])) { - $valueBool = true; - } elseif (in_array(strtolower($value), ['false', '0', 'off', 'no'])) { - $valueBool = false; - } else { - throw new AppConfigIncorrectTypeException('Value is not a boolean, please use \'true\' or \'false\''); - } - $updated = $this->appConfig->setValueBool($appName, $configName, $valueBool, $lazy); + $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; } } diff --git a/core/Command/Config/ListConfigs.php b/core/Command/Config/ListConfigs.php index 094348dd9ba..b81bfbf4d18 100644 --- a/core/Command/Config/ListConfigs.php +++ b/core/Command/Config/ListConfigs.php @@ -7,6 +7,7 @@ */ namespace OC\Core\Command\Config; +use OC\Config\ConfigManager; use OC\Core\Command\Base; use OC\SystemConfig; use OCP\IAppConfig; @@ -22,6 +23,7 @@ class ListConfigs extends Base { public function __construct( protected SystemConfig $systemConfig, protected IAppConfig $appConfig, + protected ConfigManager $configManager, ) { parent::__construct(); } @@ -44,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') ; } @@ -51,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; diff --git a/core/Command/Config/System/Base.php b/core/Command/Config/System/Base.php index ce39cd4c95b..088d902b4fd 100644 --- a/core/Command/Config/System/Base.php +++ b/core/Command/Config/System/Base.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later 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/SetConfig.php b/core/Command/Config/System/SetConfig.php index 62ab7f7120f..1b1bdc66a6e 100644 --- a/core/Command/Config/System/SetConfig.php +++ b/core/Command/Config/System/SetConfig.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface; class SetConfig extends Base { public function __construct( SystemConfig $systemConfig, + private CastHelper $castHelper, ) { parent::__construct($systemConfig); } @@ -57,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) { @@ -81,80 +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' => (float)$value, - 'readable-value' => 'double ' . (float)$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, - ]; - - case 'json': - $value = json_decode($value, true); - return [ - 'value' => $value, - 'readable-value' => 'json ' . json_encode($value), - ]; - - default: - throw new \InvalidArgumentException('Invalid type'); - } - } - - /** * @param array $configNames * @param mixed $existingValues * @param mixed $value diff --git a/core/Command/Db/ConvertFilecacheBigInt.php b/core/Command/Db/ConvertFilecacheBigInt.php index f5028aacaef..0d96d139701 100644 --- a/core/Command/Db/ConvertFilecacheBigInt.php +++ b/core/Command/Db/ConvertFilecacheBigInt.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/Db/ConvertMysqlToMB4.php b/core/Command/Db/ConvertMysqlToMB4.php index 8a2abecc804..926e56c4300 100644 --- a/core/Command/Db/ConvertMysqlToMB4.php +++ b/core/Command/Db/ConvertMysqlToMB4.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 ownCloud GmbH * SPDX-License-Identifier: AGPL-3.0-only diff --git a/core/Command/Db/ConvertType.php b/core/Command/Db/ConvertType.php index b5d1b9b9330..bca41407f68 100644 --- a/core/Command/Db/ConvertType.php +++ b/core/Command/Db/ConvertType.php @@ -13,9 +13,11 @@ use Doctrine\DBAL\Schema\Table; use OC\DB\Connection; use OC\DB\ConnectionFactory; use OC\DB\MigrationService; +use OC\DB\PgSqlTools; 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; @@ -159,7 +161,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')) { @@ -401,7 +403,7 @@ class ConvertType extends Command implements CompletionAwareInterface { $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 diff --git a/core/Command/Db/Migrations/ExecuteCommand.php b/core/Command/Db/Migrations/ExecuteCommand.php index cb6edd7c78c..a89072c1ad1 100644 --- a/core/Command/Db/Migrations/ExecuteCommand.php +++ b/core/Command/Db/Migrations/ExecuteCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH diff --git a/core/Command/Db/Migrations/GenerateCommand.php b/core/Command/Db/Migrations/GenerateCommand.php index ed29412f00b..a75280fa8b1 100644 --- a/core/Command/Db/Migrations/GenerateCommand.php +++ b/core/Command/Db/Migrations/GenerateCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 ownCloud GmbH @@ -22,8 +23,8 @@ 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); @@ -38,6 +39,7 @@ use Closure; use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; +use Override; /** * FIXME Auto-generated migration step: Please modify to your needs! @@ -49,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 { } @@ -58,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}} } @@ -67,6 +71,7 @@ class {{classname}} extends SimpleMigrationStep { * @param Closure(): ISchemaWrapper $schemaClosure * @param array $options */ + #[Override] public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { } } diff --git a/core/Command/Encryption/ChangeKeyStorageRoot.php b/core/Command/Encryption/ChangeKeyStorageRoot.php index 76cde1b8e77..3049fd2ca08 100644 --- a/core/Command/Encryption/ChangeKeyStorageRoot.php +++ b/core/Command/Encryption/ChangeKeyStorageRoot.php @@ -123,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'); } @@ -183,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/EncryptAll.php b/core/Command/Encryption/EncryptAll.php index 684591f4586..f2c991471b6 100644 --- a/core/Command/Encryption/EncryptAll.php +++ b/core/Command/Encryption/EncryptAll.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/core/Command/Encryption/ListModules.php b/core/Command/Encryption/ListModules.php index b1f35b05bc9..bf02c29f432 100644 --- a/core/Command/Encryption/ListModules.php +++ b/core/Command/Encryption/ListModules.php @@ -57,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 c2090d22d1c..937b17cde5f 100644 --- a/core/Command/Encryption/MigrateKeyStorage.php +++ b/core/Command/Encryption/MigrateKeyStorage.php @@ -80,10 +80,10 @@ 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); @@ -127,10 +127,10 @@ 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); diff --git a/core/Command/Group/AddUser.php b/core/Command/Group/AddUser.php index 1f144b13893..999113390af 100644 --- a/core/Command/Group/AddUser.php +++ b/core/Command/Group/AddUser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/Group/ListCommand.php b/core/Command/Group/ListCommand.php index a3622585d18..01522a23f7f 100644 --- a/core/Command/Group/ListCommand.php +++ b/core/Command/Group/ListCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/Group/RemoveUser.php b/core/Command/Group/RemoveUser.php index 7c58f9ac4c4..952fc6e7712 100644 --- a/core/Command/Group/RemoveUser.php +++ b/core/Command/Group/RemoveUser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/Info/File.php b/core/Command/Info/File.php index c21c2e666fb..287bd0e29cb 100644 --- a/core/Command/Info/File.php +++ b/core/Command/Info/File.php @@ -9,6 +9,7 @@ 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; @@ -176,7 +177,7 @@ class File extends Command { if ($input->getOption('storage-tree')) { $storageTmp = $storage; $storageClass = get_class($storageTmp) . ' (cache:' . get_class($storageTmp->getCache()) . ')'; - while ($storageTmp instanceof \OC\Files\Storage\Wrapper\Wrapper) { + while ($storageTmp instanceof Wrapper) { $storageTmp = $storageTmp->getWrapperStorage(); $storageClass .= "\n\t" . '> ' . get_class($storageTmp) . ' (cache:' . get_class($storageTmp->getCache()) . ')'; } diff --git a/core/Command/Info/FileUtils.php b/core/Command/Info/FileUtils.php index 5de5f5fcaa6..bc07535a289 100644 --- a/core/Command/Info/FileUtils.php +++ b/core/Command/Info/FileUtils.php @@ -8,11 +8,13 @@ declare(strict_types=1); 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; @@ -21,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(); @@ -218,4 +226,100 @@ class FileUtils { } 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/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 e1889a35cfe..0145a3f8070 100644 --- a/core/Command/Integrity/CheckApp.php +++ b/core/Command/Integrity/CheckApp.php @@ -40,31 +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'); - if ($path === '') { - $path = $this->appLocator->getAppPath($appid); + if ($input->getOption('all') && $input->getArgument('appid')) { + $output->writeln('<error>Option "--all" cannot be combined with an appid</error>'); + return 1; } - 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); - $this->writeArrayInOutputFormat($input, $output, $result); - if (count($result) > 0) { - $output->writeln('<error>' . count($result) . ' errors found</error>', OutputInterface::VERBOSITY_VERBOSE); - 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; + } + + 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); - } else { - $output->writeln('<comment>App signature not found, skipping app integrity check</comment>'); + return 0; } - return 0; + + return 1; } } diff --git a/core/Command/Log/File.php b/core/Command/Log/File.php index 8b4a38db611..ba5dad956e9 100644 --- a/core/Command/Log/File.php +++ b/core/Command/Log/File.php @@ -8,6 +8,7 @@ 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; @@ -61,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; } @@ -87,7 +88,7 @@ class File extends Command implements Completion\CompletionAwareInterface { $rotateSize = $this->config->getSystemValue('log_rotate_size', 100 * 1024 * 1024); if ($rotateSize) { - $rotateString = \OCP\Util::humanFileSize($rotateSize); + $rotateString = Util::humanFileSize($rotateSize); } else { $rotateString = 'disabled'; } diff --git a/core/Command/Maintenance/Install.php b/core/Command/Maintenance/Install.php index d23a8d30070..6170c5a2638 100644 --- a/core/Command/Maintenance/Install.php +++ b/core/Command/Maintenance/Install.php @@ -15,6 +15,7 @@ use OC\Console\TimestampFormatter; use OC\Migration\ConsoleOutput; use OC\Setup; use OC\SystemConfig; +use OCP\Server; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -43,6 +44,7 @@ class Install extends Command { ->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('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') @@ -51,15 +53,15 @@ class Install extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { // validate the environment - $setupHelper = \OCP\Server::get(\OC\Setup::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; } } @@ -119,6 +121,7 @@ class Install extends Command { if ($input->hasParameterOption('--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'); @@ -141,7 +144,7 @@ class Install extends Command { } } - 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 . '>?'); @@ -150,7 +153,7 @@ class Install extends Command { $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 . '>.'); } @@ -160,6 +163,7 @@ class Install extends Command { 'dbpass' => $dbPass, 'dbname' => $dbName, 'dbhost' => $dbHost, + 'admindisable' => $disableAdminUser, 'adminlogin' => $adminLogin, 'adminpass' => $adminPassword, 'adminemail' => $adminEmail, diff --git a/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php index 5b598d20672..f8f19a61993 100644 --- a/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php +++ b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php @@ -15,7 +15,7 @@ class GenerateMimetypeFileBuilder { * @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 @@ -71,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 @@ -83,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/UpdateJS.php b/core/Command/Maintenance/Mimetype/UpdateJS.php index 35633f16355..2132ff54c6d 100644 --- a/core/Command/Maintenance/Mimetype/UpdateJS.php +++ b/core/Command/Maintenance/Mimetype/UpdateJS.php @@ -32,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/UpdateHtaccess.php b/core/Command/Maintenance/UpdateHtaccess.php index 4939e368e2d..eeff3bf8c62 100644 --- a/core/Command/Maintenance/UpdateHtaccess.php +++ b/core/Command/Maintenance/UpdateHtaccess.php @@ -7,6 +7,7 @@ */ 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; @@ -19,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 f819b9c8e58..3fbcb546cca 100644 --- a/core/Command/Maintenance/UpdateTheme.php +++ b/core/Command/Maintenance/UpdateTheme.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later 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/Preview/Repair.php b/core/Command/Preview/Repair.php index 3ccd8231300..a92a4cf8ed0 100644 --- a/core/Command/Preview/Repair.php +++ b/core/Command/Preview/Repair.php @@ -86,7 +86,7 @@ class Repair extends Command { $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(); 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/SetupChecks.php b/core/Command/SetupChecks.php index 60517e224b3..6ef67726839 100644 --- a/core/Command/SetupChecks.php +++ b/core/Command/SetupChecks.php @@ -61,12 +61,12 @@ class SetupChecks extends Base { $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/SystemTag/Add.php b/core/Command/SystemTag/Add.php index 92ed42c37bc..df8b507b07d 100644 --- a/core/Command/SystemTag/Add.php +++ b/core/Command/SystemTag/Add.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/SystemTag/Delete.php b/core/Command/SystemTag/Delete.php index 73b3dc8187a..f657f4473ab 100644 --- a/core/Command/SystemTag/Delete.php +++ b/core/Command/SystemTag/Delete.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/SystemTag/Edit.php b/core/Command/SystemTag/Edit.php index 614f2798ce4..09c662e58e9 100644 --- a/core/Command/SystemTag/Edit.php +++ b/core/Command/SystemTag/Edit.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/SystemTag/ListCommand.php b/core/Command/SystemTag/ListCommand.php index 836869f157d..2c6435d6faf 100644 --- a/core/Command/SystemTag/ListCommand.php +++ b/core/Command/SystemTag/ListCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TaskProcessing/EnabledCommand.php b/core/Command/TaskProcessing/EnabledCommand.php index b382de12a81..7195d19a7a4 100644 --- a/core/Command/TaskProcessing/EnabledCommand.php +++ b/core/Command/TaskProcessing/EnabledCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -47,15 +48,15 @@ class EnabledCommand extends Base { } else { $taskTypeSettings = json_decode($json, true, flags: JSON_THROW_ON_ERROR); } - + $taskTypeSettings[$taskType] = $enabled; - + $this->config->setAppValue('core', 'ai.taskprocessing_type_preferences', json_encode($taskTypeSettings)); $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 index a61ddbe1621..5c4fd17f2f8 100644 --- a/core/Command/TaskProcessing/GetCommand.php +++ b/core/Command/TaskProcessing/GetCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TaskProcessing/ListCommand.php b/core/Command/TaskProcessing/ListCommand.php index f4ea76729d9..81eb258d35d 100644 --- a/core/Command/TaskProcessing/ListCommand.php +++ b/core/Command/TaskProcessing/ListCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TaskProcessing/Statistics.php b/core/Command/TaskProcessing/Statistics.php index a3dc9ee0254..86478b34db1 100644 --- a/core/Command/TaskProcessing/Statistics.php +++ b/core/Command/TaskProcessing/Statistics.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TwoFactorAuth/Base.php b/core/Command/TwoFactorAuth/Base.php index 70e33bfd23c..034ea36afca 100644 --- a/core/Command/TwoFactorAuth/Base.php +++ b/core/Command/TwoFactorAuth/Base.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/Upgrade.php b/core/Command/Upgrade.php index 6220c9a70d4..c3d6aacc714 100644 --- a/core/Command/Upgrade.php +++ b/core/Command/Upgrade.php @@ -21,6 +21,7 @@ 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; @@ -63,11 +64,11 @@ class Upgrade extends Command { } $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 { @@ -132,17 +133,17 @@ 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>'; } else { @@ -150,42 +151,42 @@ class Upgrade extends Command { } $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, &$incompatibleOverwrites) { + $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) { + $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) { + $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) { + $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) { + $updater->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($output): void { $output->writeln('<info>Finished code integrity check</info>'); }); @@ -226,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 033d2bdc9a2..4de4e247991 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -52,7 +52,7 @@ class Add extends Command { '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', @@ -91,10 +91,10 @@ class Add extends Command { // 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')) { diff --git a/core/Command/User/AuthTokens/Add.php b/core/Command/User/AuthTokens/Add.php index ad4bf732bd0..89b20535c63 100644 --- a/core/Command/User/AuthTokens/Add.php +++ b/core/Command/User/AuthTokens/Add.php @@ -62,9 +62,9 @@ class Add extends Command { } 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()) { diff --git a/core/Command/User/AuthTokens/Delete.php b/core/Command/User/AuthTokens/Delete.php index f2c75a8ad99..2047d2eae2a 100644 --- a/core/Command/User/AuthTokens/Delete.php +++ b/core/Command/User/AuthTokens/Delete.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/User/AuthTokens/ListCommand.php b/core/Command/User/AuthTokens/ListCommand.php index 1ebd4a0f0b4..b36aa717505 100644 --- a/core/Command/User/AuthTokens/ListCommand.php +++ b/core/Command/User/AuthTokens/ListCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/User/Info.php b/core/Command/User/Info.php index 6487b533fa2..e7fc9286e74 100644 --- a/core/Command/User/Info.php +++ b/core/Command/User/Info.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -6,6 +7,7 @@ namespace OC\Core\Command\User; use OC\Core\Command\Base; +use OCP\Files\NotFoundException; use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; @@ -84,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/LastSeen.php b/core/Command/User/LastSeen.php index dbb611a4fff..984def72cd6 100644 --- a/core/Command/User/LastSeen.php +++ b/core/Command/User/LastSeen.php @@ -68,7 +68,7 @@ class LastSeen extends Base { return 1; } - $this->userManager->callForAllUsers(static function (IUser $user) use ($output) { + $this->userManager->callForAllUsers(static function (IUser $user) use ($output): void { $lastLogin = $user->getLastLogin(); if ($lastLogin === 0) { $output->writeln($user->getUID() . ' has never logged in.'); diff --git a/core/Command/User/ListCommand.php b/core/Command/User/ListCommand.php index e7fb3de71f0..66b831c793b 100644 --- a/core/Command/User/ListCommand.php +++ b/core/Command/User/ListCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later 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/ResetPassword.php b/core/Command/User/ResetPassword.php index 2f18c3d473e..0e8b1325770 100644 --- a/core/Command/User/ResetPassword.php +++ b/core/Command/User/ResetPassword.php @@ -41,7 +41,7 @@ class ResetPassword extends Base { 'password-from-env', null, InputOption::VALUE_NONE, - 'read password from environment variable OC_PASS' + 'read password from environment variable NC_PASS/OC_PASS' ) ; } @@ -56,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()) { diff --git a/core/Command/User/SyncAccountDataCommand.php b/core/Command/User/SyncAccountDataCommand.php index 640b66581e1..c353df6fe9f 100644 --- a/core/Command/User/SyncAccountDataCommand.php +++ b/core/Command/User/SyncAccountDataCommand.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -16,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(); } diff --git a/core/Command/User/Welcome.php b/core/Command/User/Welcome.php index c383811f982..65637759689 100644 --- a/core/Command/User/Welcome.php +++ b/core/Command/User/Welcome.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 FedericoHeichou <federicoheichou@gmail.com> * SPDX-License-Identifier: AGPL-3.0-or-later @@ -15,24 +16,15 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Welcome extends Base { - /** @var IUserManager */ - protected $userManager; - - /** @var NewUserMailHelper */ - private $newUserMailHelper; - /** * @param IUserManager $userManager * @param NewUserMailHelper $newUserMailHelper */ public function __construct( - IUserManager $userManager, - NewUserMailHelper $newUserMailHelper, + protected IUserManager $userManager, + private NewUserMailHelper $newUserMailHelper, ) { parent::__construct(); - - $this->userManager = $userManager; - $this->newUserMailHelper = $newUserMailHelper; } /** |