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