aboutsummaryrefslogtreecommitdiffstats
path: root/core/Command/Maintenance
diff options
context:
space:
mode:
Diffstat (limited to 'core/Command/Maintenance')
-rw-r--r--core/Command/Maintenance/DataFingerprint.php36
-rw-r--r--core/Command/Maintenance/Install.php207
-rw-r--r--core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php100
-rw-r--r--core/Command/Maintenance/Mimetype/UpdateDB.php79
-rw-r--r--core/Command/Maintenance/Mimetype/UpdateJS.php41
-rw-r--r--core/Command/Maintenance/Mode.php68
-rw-r--r--core/Command/Maintenance/Repair.php124
-rw-r--r--core/Command/Maintenance/RepairShareOwnership.php176
-rw-r--r--core/Command/Maintenance/UpdateHtaccess.php31
-rw-r--r--core/Command/Maintenance/UpdateTheme.php40
10 files changed, 902 insertions, 0 deletions
diff --git a/core/Command/Maintenance/DataFingerprint.php b/core/Command/Maintenance/DataFingerprint.php
new file mode 100644
index 00000000000..014d6c411a4
--- /dev/null
+++ b/core/Command/Maintenance/DataFingerprint.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class DataFingerprint extends Command {
+ public function __construct(
+ protected IConfig $config,
+ protected ITimeFactory $timeFactory,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('maintenance:data-fingerprint')
+ ->setDescription('update the systems data-fingerprint after a backup is restored');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $fingerPrint = md5($this->timeFactory->getTime());
+ $this->config->setSystemValue('data-fingerprint', $fingerPrint);
+ $output->writeln('<info>Updated data-fingerprint to ' . $fingerPrint . '</info>');
+ return 0;
+ }
+}
diff --git a/core/Command/Maintenance/Install.php b/core/Command/Maintenance/Install.php
new file mode 100644
index 00000000000..6170c5a2638
--- /dev/null
+++ b/core/Command/Maintenance/Install.php
@@ -0,0 +1,207 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance;
+
+use bantu\IniGetWrapper\IniGetWrapper;
+use InvalidArgumentException;
+use OC\Console\TimestampFormatter;
+use OC\Migration\ConsoleOutput;
+use OC\Setup;
+use OC\SystemConfig;
+use OCP\Server;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\Question;
+use Throwable;
+use function get_class;
+
+class Install extends Command {
+ public function __construct(
+ private SystemConfig $config,
+ private IniGetWrapper $iniGetWrapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('maintenance:install')
+ ->setDescription('install Nextcloud')
+ ->addOption('database', null, InputOption::VALUE_REQUIRED, 'Supported database type', 'sqlite')
+ ->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, 'Login to connect to the database')
+ ->addOption('database-pass', null, InputOption::VALUE_OPTIONAL, 'Password of the database user', null)
+ ->addOption('database-table-space', null, InputOption::VALUE_OPTIONAL, 'Table space of the database (oci only)', null)
+ ->addOption('disable-admin-user', null, InputOption::VALUE_NONE, 'Disable the creation of an admin user')
+ ->addOption('admin-user', null, InputOption::VALUE_REQUIRED, 'Login of the admin account', 'admin')
+ ->addOption('admin-pass', null, InputOption::VALUE_REQUIRED, 'Password of the admin account')
+ ->addOption('admin-email', null, InputOption::VALUE_OPTIONAL, 'E-Mail of the admin account')
+ ->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
+ $setupHelper = Server::get(Setup::class);
+ $sysInfo = $setupHelper->getSystemInfo(true);
+ $errors = $sysInfo['errors'];
+ if (count($errors) > 0) {
+ $this->printErrors($output, $errors);
+
+ // ignore the OS X setup warning
+ if (count($errors) !== 1
+ || (string)$errors[0]['error'] !== 'Mac OS X is not supported and Nextcloud will not work properly on this platform. Use it at your own risk!') {
+ return 1;
+ }
+ }
+
+ // 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, $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;
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @param string[] $supportedDatabases
+ * @return array
+ */
+ protected function validateInput(InputInterface $input, OutputInterface $output, $supportedDatabases) {
+ $db = strtolower($input->getOption('database'));
+
+ if (!in_array($db, $supportedDatabases)) {
+ throw new InvalidArgumentException("Database <$db> is not supported. " . implode(', ', $supportedDatabases) . ' are supported.');
+ }
+
+ $dbUser = $input->getOption('database-user');
+ $dbPass = $input->getOption('database-pass');
+ $dbName = $input->getOption('database-name');
+ $dbPort = $input->getOption('database-port');
+ if ($db === 'oci') {
+ // an empty hostname needs to be read from the raw parameters
+ $dbHost = $input->getParameterOption('--database-host', '');
+ } else {
+ $dbHost = $input->getOption('database-host');
+ }
+ if ($dbPort) {
+ // Append the port to the host so it is the same as in the config (there is no dbport config)
+ $dbHost .= ':' . $dbPort;
+ }
+ if ($input->hasParameterOption('--database-pass')) {
+ $dbPass = (string)$input->getOption('database-pass');
+ }
+ $disableAdminUser = (bool)$input->getOption('disable-admin-user');
+ $adminLogin = $input->getOption('admin-user');
+ $adminPassword = $input->getOption('admin-pass');
+ $adminEmail = $input->getOption('admin-email');
+ $dataDir = $input->getOption('data-dir');
+
+ if ($db !== 'sqlite') {
+ if (is_null($dbUser)) {
+ throw new InvalidArgumentException('Database account not provided.');
+ }
+ if (is_null($dbName)) {
+ throw new InvalidArgumentException('Database name not provided.');
+ }
+ if (is_null($dbPass)) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new Question('What is the password to access the database with user <' . $dbUser . '>?');
+ $question->setHidden(true);
+ $question->setHiddenFallback(false);
+ $dbPass = $helper->ask($input, $output, $question);
+ }
+ }
+
+ if (!$disableAdminUser && $adminPassword === null) {
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+ $question = new Question('What is the password you like to use for the admin account <' . $adminLogin . '>?');
+ $question->setHidden(true);
+ $question->setHiddenFallback(false);
+ $adminPassword = $helper->ask($input, $output, $question);
+ }
+
+ if (!$disableAdminUser && $adminEmail !== null && !filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
+ throw new InvalidArgumentException('Invalid e-mail-address <' . $adminEmail . '> for <' . $adminLogin . '>.');
+ }
+
+ $options = [
+ 'dbtype' => $db,
+ 'dbuser' => $dbUser,
+ 'dbpass' => $dbPass,
+ 'dbname' => $dbName,
+ 'dbhost' => $dbHost,
+ 'admindisable' => $disableAdminUser,
+ 'adminlogin' => $adminLogin,
+ 'adminpass' => $adminPassword,
+ 'adminemail' => $adminEmail,
+ 'directory' => $dataDir
+ ];
+ if ($db === 'oci') {
+ $options['dbtablespace'] = $input->getParameterOption('--database-table-space', '');
+ }
+ return $options;
+ }
+
+ /**
+ * @param OutputInterface $output
+ * @param array<string|array> $errors
+ */
+ protected function printErrors(OutputInterface $output, array $errors): void {
+ foreach ($errors as $error) {
+ if (is_array($error)) {
+ $output->writeln('<error>' . $error['error'] . '</error>');
+ if (isset($error['hint']) && !empty($error['hint'])) {
+ $output->writeln('<info> -> ' . $error['hint'] . '</info>');
+ }
+ if (isset($error['exception']) && $error['exception'] instanceof Throwable) {
+ $this->printThrowable($output, $error['exception']);
+ }
+ } else {
+ $output->writeln('<error>' . $error . '</error>');
+ }
+ }
+ }
+
+ private function printThrowable(OutputInterface $output, Throwable $t): void {
+ $output->write('<info>Trace: ' . $t->getTraceAsString() . '</info>');
+ $output->writeln('');
+ if ($t->getPrevious() !== null) {
+ $output->writeln('');
+ $output->writeln('<info>Previous: ' . get_class($t->getPrevious()) . ': ' . $t->getPrevious()->getMessage() . '</info>');
+ $this->printThrowable($output, $t->getPrevious());
+ }
+ }
+}
diff --git a/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php
new file mode 100644
index 00000000000..f8f19a61993
--- /dev/null
+++ b/core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\Maintenance\Mimetype;
+
+class GenerateMimetypeFileBuilder {
+ /**
+ * Generate mime type list file
+ *
+ * @param array<string,string> $aliases
+ * @return string
+ */
+ public function generateFile(array $aliases, array $names): string {
+ // Remove comments
+ $aliases = array_filter($aliases, static function ($key) {
+ // Single digit extensions will be treated as integers
+ // Let's make sure they are strings
+ // https://github.com/nextcloud/server/issues/42902
+ $key = (string)$key;
+ return !($key === '' || $key[0] === '_');
+ }, ARRAY_FILTER_USE_KEY);
+
+ // Fetch all files
+ $dir = new \DirectoryIterator(\OC::$SERVERROOT . '/core/img/filetypes');
+
+ $files = [];
+ foreach ($dir as $fileInfo) {
+ if ($fileInfo->isFile()) {
+ $file = preg_replace('/.[^.]*$/', '', $fileInfo->getFilename());
+ $files[] = $file;
+ }
+ }
+
+ //Remove duplicates
+ $files = array_values(array_unique($files));
+ sort($files);
+
+ // Fetch all themes!
+ $themes = [];
+ $dirs = new \DirectoryIterator(\OC::$SERVERROOT . '/themes/');
+ foreach ($dirs as $dir) {
+ //Valid theme dir
+ if ($dir->isFile() || $dir->isDot()) {
+ continue;
+ }
+
+ $theme = $dir->getFilename();
+ $themeDir = $dir->getPath() . '/' . $theme . '/core/img/filetypes/';
+ // Check if this theme has its own filetype icons
+ if (!file_exists($themeDir)) {
+ continue;
+ }
+
+ $themes[$theme] = [];
+ // Fetch all the theme icons!
+ $themeIt = new \DirectoryIterator($themeDir);
+ foreach ($themeIt as $fileInfo) {
+ if ($fileInfo->isFile()) {
+ $file = preg_replace('/.[^.]*$/', '', $fileInfo->getFilename());
+ $themes[$theme][] = $file;
+ }
+ }
+
+ //Remove Duplicates
+ $themes[$theme] = array_values(array_unique($themes[$theme]));
+ sort($themes[$theme]);
+ }
+
+ $namesOutput = '';
+ foreach ($names as $key => $name) {
+ if (str_starts_with($key, '_') || trim($name) === '') {
+ // Skip internal names
+ continue;
+ }
+ $namesOutput .= "'$key': t('core', " . json_encode($name) . "),\n";
+ }
+
+ //Generate the JS
+ return '/**
+* This file is automatically generated
+* DO NOT EDIT MANUALLY!
+*
+* You can update the list of MimeType Aliases in config/mimetypealiases.json
+* The list of files is fetched from core/img/filetypes
+* To regenerate this file run ./occ maintenance:mimetype:update-js
+*/
+OC.MimeTypeList={
+ aliases: ' . json_encode($aliases, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . ',
+ files: ' . json_encode($files, JSON_PRETTY_PRINT) . ',
+ themes: ' . json_encode($themes, JSON_PRETTY_PRINT) . ',
+ names: {' . $namesOutput . '},
+};
+';
+ }
+}
diff --git a/core/Command/Maintenance/Mimetype/UpdateDB.php b/core/Command/Maintenance/Mimetype/UpdateDB.php
new file mode 100644
index 00000000000..4467e89eb32
--- /dev/null
+++ b/core/Command/Maintenance/Mimetype/UpdateDB.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance\Mimetype;
+
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\IMimeTypeLoader;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class UpdateDB extends Command {
+ public const DEFAULT_MIMETYPE = 'application/octet-stream';
+
+ public function __construct(
+ protected IMimeTypeDetector $mimetypeDetector,
+ protected IMimeTypeLoader $mimetypeLoader,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('maintenance:mimetype:update-db')
+ ->setDescription('Update database mimetypes and update filecache')
+ ->addOption(
+ 'repair-filecache',
+ null,
+ InputOption::VALUE_NONE,
+ 'Repair filecache for all mimetypes, not just new ones'
+ )
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $mappings = $this->mimetypeDetector->getAllMappings();
+
+ $totalFilecacheUpdates = 0;
+ $totalNewMimetypes = 0;
+
+ foreach ($mappings as $ext => $mimetypes) {
+ // Single digit extensions will be treated as integers
+ // Let's make sure they are strings
+ // https://github.com/nextcloud/server/issues/42902
+ $ext = (string)$ext;
+ if ($ext[0] === '_') {
+ // comment
+ continue;
+ }
+ $mimetype = $mimetypes[0];
+ $existing = $this->mimetypeLoader->exists($mimetype);
+ // this will add the mimetype if it didn't exist
+ $mimetypeId = $this->mimetypeLoader->getId($mimetype);
+
+ if (!$existing) {
+ $output->writeln('Added mimetype "' . $mimetype . '" to database');
+ $totalNewMimetypes++;
+ }
+
+ if (!$existing || $input->getOption('repair-filecache')) {
+ $touchedFilecacheRows = $this->mimetypeLoader->updateFilecache($ext, $mimetypeId);
+ if ($touchedFilecacheRows > 0) {
+ $output->writeln('Updated ' . $touchedFilecacheRows . ' filecache rows for mimetype "' . $mimetype . '"');
+ }
+ $totalFilecacheUpdates += $touchedFilecacheRows;
+ }
+ }
+
+ $output->writeln('Added ' . $totalNewMimetypes . ' new mimetypes');
+ $output->writeln('Updated ' . $totalFilecacheUpdates . ' filecache rows');
+ return 0;
+ }
+}
diff --git a/core/Command/Maintenance/Mimetype/UpdateJS.php b/core/Command/Maintenance/Mimetype/UpdateJS.php
new file mode 100644
index 00000000000..2132ff54c6d
--- /dev/null
+++ b/core/Command/Maintenance/Mimetype/UpdateJS.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance\Mimetype;
+
+use OCP\Files\IMimeTypeDetector;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+
+use Symfony\Component\Console\Output\OutputInterface;
+
+class UpdateJS extends Command {
+ public function __construct(
+ protected IMimeTypeDetector $mimetypeDetector,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('maintenance:mimetype:update-js')
+ ->setDescription('Update mimetypelist.js');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ // Fetch all the aliases
+ $aliases = $this->mimetypeDetector->getAllAliases();
+
+ // Output the JS
+ $generatedMimetypeFile = new GenerateMimetypeFileBuilder();
+ $namings = $this->mimetypeDetector->getAllNamings();
+ file_put_contents(\OC::$SERVERROOT . '/core/js/mimetypelist.js', $generatedMimetypeFile->generateFile($aliases, $namings));
+
+ $output->writeln('<info>mimetypelist.js is updated');
+ return 0;
+ }
+}
diff --git a/core/Command/Maintenance/Mode.php b/core/Command/Maintenance/Mode.php
new file mode 100644
index 00000000000..853e843f57b
--- /dev/null
+++ b/core/Command/Maintenance/Mode.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance;
+
+use OCP\IConfig;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Mode extends Command {
+ public function __construct(
+ protected IConfig $config,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('maintenance:mode')
+ ->setDescription('Show or toggle maintenance mode status')
+ ->setHelp('Maintenance mode prevents new logins, locks existing sessions, and disables background jobs.')
+ ->addOption(
+ 'on',
+ null,
+ InputOption::VALUE_NONE,
+ 'enable maintenance mode'
+ )
+ ->addOption(
+ 'off',
+ null,
+ InputOption::VALUE_NONE,
+ 'disable maintenance mode'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $maintenanceMode = $this->config->getSystemValueBool('maintenance');
+ if ($input->getOption('on')) {
+ if ($maintenanceMode === false) {
+ $this->config->setSystemValue('maintenance', true);
+ $output->writeln('Maintenance mode enabled');
+ } else {
+ $output->writeln('Maintenance mode already enabled');
+ }
+ } elseif ($input->getOption('off')) {
+ if ($maintenanceMode === true) {
+ $this->config->setSystemValue('maintenance', false);
+ $output->writeln('Maintenance mode disabled');
+ } else {
+ $output->writeln('Maintenance mode already disabled');
+ }
+ } else {
+ if ($maintenanceMode) {
+ $output->writeln('Maintenance mode is currently enabled');
+ } else {
+ $output->writeln('Maintenance mode is currently disabled');
+ }
+ }
+ return 0;
+ }
+}
diff --git a/core/Command/Maintenance/Repair.php b/core/Command/Maintenance/Repair.php
new file mode 100644
index 00000000000..f0c88f6811b
--- /dev/null
+++ b/core/Command/Maintenance/Repair.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance;
+
+use 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;
+
+class Repair extends Command {
+ private ProgressBar $progress;
+ private OutputInterface $output;
+ protected bool $errored = false;
+
+ public function __construct(
+ protected \OC\Repair $repair,
+ protected IConfig $config,
+ private IEventDispatcher $dispatcher,
+ private IAppManager $appManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('maintenance:repair')
+ ->setDescription('repair this installation')
+ ->addOption(
+ 'include-expensive',
+ null,
+ InputOption::VALUE_NONE,
+ 'Use this option when you want to include resource and load expensive tasks');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $repairSteps = $this->repair::getRepairSteps();
+
+ if ($input->getOption('include-expensive')) {
+ $repairSteps = array_merge($repairSteps, $this->repair::getExpensiveRepairSteps());
+ }
+
+ foreach ($repairSteps as $step) {
+ $this->repair->addStep($step);
+ }
+
+ $apps = $this->appManager->getEnabledApps();
+ foreach ($apps as $app) {
+ if (!$this->appManager->isEnabledForUser($app)) {
+ continue;
+ }
+ $info = $this->appManager->getAppInfo($app);
+ if (!is_array($info)) {
+ continue;
+ }
+ $this->appManager->loadApp($app);
+ $steps = $info['repair-steps']['post-migration'];
+ foreach ($steps as $step) {
+ try {
+ $this->repair->addStep($step);
+ } catch (Exception $ex) {
+ $output->writeln("<error>Failed to load repair step for $app: {$ex->getMessage()}</error>");
+ }
+ }
+ }
+
+
+
+ $maintenanceMode = $this->config->getSystemValueBool('maintenance');
+ $this->config->setSystemValue('maintenance', true);
+
+ $this->progress = new ProgressBar($output);
+ $this->output = $output;
+ $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 $this->errored ? 1 : 0;
+ }
+
+ 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..16675545afe
--- /dev/null
+++ b/core/Command/Maintenance/RepairShareOwnership.php
@@ -0,0 +1,176 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+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\Helper\QuestionHelper;
+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) {
+ /** @var QuestionHelper $helper */
+ $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($qb->expr()->castColumn('s.item_source', IQueryBuilder::PARAM_INT), 'f.fileid'))
+ ->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id'))
+ ->where($qb->expr()->neq('m.user_id', 's.uid_owner'))
+ ->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), 'm.user_id', $qb->expr()->literal('/')), 'm.mount_point'))
+ ->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
new file mode 100644
index 00000000000..eeff3bf8c62
--- /dev/null
+++ b/core/Command/Maintenance/UpdateHtaccess.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Core\Command\Maintenance;
+
+use OC\Setup;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class UpdateHtaccess extends Command {
+ protected function configure() {
+ $this
+ ->setName('maintenance:update:htaccess')
+ ->setDescription('Updates the .htaccess file');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ if (Setup::updateHtaccess()) {
+ $output->writeln('.htaccess has been updated');
+ return 0;
+ } else {
+ $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
new file mode 100644
index 00000000000..3fbcb546cca
--- /dev/null
+++ b/core/Command/Maintenance/UpdateTheme.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Command\Maintenance;
+
+use OC\Core\Command\Maintenance\Mimetype\UpdateJS;
+use OCP\Files\IMimeTypeDetector;
+use OCP\ICacheFactory;
+use Symfony\Component\Console\Input\InputInterface;
+
+use Symfony\Component\Console\Output\OutputInterface;
+
+class UpdateTheme extends UpdateJS {
+ public function __construct(
+ IMimeTypeDetector $mimetypeDetector,
+ protected ICacheFactory $cacheFactory,
+ ) {
+ parent::__construct($mimetypeDetector);
+ }
+
+ protected function configure() {
+ $this
+ ->setName('maintenance:theme:update')
+ ->setDescription('Apply custom theme changes');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ // run mimetypelist.js update since themes might change mimetype icons
+ parent::execute($input, $output);
+
+ // cleanup image cache
+ $c = $this->cacheFactory->createDistributed('imagePath');
+ $c->clear('');
+ $output->writeln('<info>Image cache cleared</info>');
+ return 0;
+ }
+}