diff options
Diffstat (limited to 'core/Command/Maintenance')
-rw-r--r-- | core/Command/Maintenance/DataFingerprint.php | 36 | ||||
-rw-r--r-- | core/Command/Maintenance/Install.php | 207 | ||||
-rw-r--r-- | core/Command/Maintenance/Mimetype/GenerateMimetypeFileBuilder.php | 100 | ||||
-rw-r--r-- | core/Command/Maintenance/Mimetype/UpdateDB.php | 79 | ||||
-rw-r--r-- | core/Command/Maintenance/Mimetype/UpdateJS.php | 41 | ||||
-rw-r--r-- | core/Command/Maintenance/Mode.php | 68 | ||||
-rw-r--r-- | core/Command/Maintenance/Repair.php | 124 | ||||
-rw-r--r-- | core/Command/Maintenance/RepairShareOwnership.php | 176 | ||||
-rw-r--r-- | core/Command/Maintenance/UpdateHtaccess.php | 31 | ||||
-rw-r--r-- | core/Command/Maintenance/UpdateTheme.php | 40 |
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; + } +} |