diff --git a/core/Command/Maintenance/Repair.php b/core/Command/Maintenance/Repair.php index a1a75099214e..dd368fe5fb21 100644 --- a/core/Command/Maintenance/Repair.php +++ b/core/Command/Maintenance/Repair.php @@ -5,6 +5,7 @@ * @author Robin Appelman * @author Thomas Müller * @author Vincent Petry + * @author Semih Serhat Karakaya * * @copyright Copyright (c) 2018, ownCloud GmbH * @license AGPL-3.0 @@ -26,7 +27,9 @@ namespace OC\Core\Command\Maintenance; use Exception; +use OCP\App\IAppManager; use OCP\IConfig; +use OCP\Migration\IRepairStep; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; @@ -38,23 +41,33 @@ class Repair extends Command { /** @var \OC\Repair $repair */ protected $repair; - /** @var IConfig */ + /** @var IConfig $config */ protected $config; - /** @var EventDispatcherInterface */ + /** @var EventDispatcherInterface $dispatcher */ private $dispatcher; + /** @var IAppManager $appManager */ + private $appManager; /** @var ProgressBar */ private $progress; - /** @var OutputInterface */ + /** @var OutputInterface $output */ private $output; /** * @param \OC\Repair $repair * @param IConfig $config + * @param EventDispatcherInterface $dispatcher + * @param IAppManager $appManager */ - public function __construct(\OC\Repair $repair, IConfig $config, EventDispatcherInterface $dispatcher) { + public function __construct( + \OC\Repair $repair, + IConfig $config, + EventDispatcherInterface $dispatcher, + IAppManager $appManager + ) { $this->repair = $repair; $this->config = $config; $this->dispatcher = $dispatcher; + $this->appManager = $appManager; parent::__construct(); } @@ -62,6 +75,18 @@ protected function configure() { $this ->setName('maintenance:repair') ->setDescription('Repair the installation.') + ->addOption( + 'list', + null, + InputOption::VALUE_NONE, + 'Lists all possible repair steps' + ) + ->addOption( + 'single', + 's', + InputOption::VALUE_REQUIRED, + 'Run just one repair step given its class name' + ) ->addOption( 'include-expensive', null, @@ -70,49 +95,67 @@ protected function configure() { } protected function execute(InputInterface $input, OutputInterface $output) { - $includeExpensive = $input->getOption('include-expensive'); - if ($includeExpensive) { - foreach ($this->repair->getExpensiveRepairSteps() as $step) { - $this->repair->addStep($step); + $appSteps = $this->getAppsRepairSteps($output); + // Handle listing repair steps + $steps = \array_merge( + \OC\Repair::getRepairSteps(), + \OC\Repair::getExpensiveRepairSteps(), + $appSteps + ); + $list = $input->getOption('list'); + if ($list) { + $output->writeln("Found ".\count($steps)." repair steps"); + $output->writeln(""); + foreach ($steps as $step) { + $output->writeln(\get_class($step) . " -> " . $step->getName()); } + return 0; } - $apps = \OC::$server->getAppManager()->getInstalledApps(); - foreach ($apps as $app) { - if (!\OC_App::isEnabled($app)) { - continue; - } - $info = \OC_App::getAppInfo($app); - if (!\is_array($info)) { - continue; + $maintenanceMode = $this->config->getSystemValue('maintenance', false); + if ($maintenanceMode !== true) { + $output->writeln("Turn on maintenance mode to use this command."); + return 0; + } + + // Handle running just one repair step + $single = $input->getOption('single'); + if ($single) { + // Check it exists + $stepNames = \array_map('get_class', $steps); + if (!\in_array($single, $stepNames, true)) { + $output->writeln("Repair step not found. Use --list to show available steps."); + return 1; } - \OC_App::loadApp($app); - $steps = $info['repair-steps']['post-migration']; + // Find step and create repair manager + $repair = new \OC\Repair([], $this->dispatcher); foreach ($steps as $step) { - try { - $this->repair->addStep($step); - } catch (Exception $ex) { - $output->writeln("Failed to load repair step for $app: {$ex->getMessage()}"); + if (\get_class($step) === $single) { + $repair->addStep($step); + break; } } + + $this->initializeProgressBar($output); + $repair->run(); + return 0; } - $maintenanceMode = $this->config->getSystemValue('maintenance', false); - $this->config->setSystemValue('maintenance', true); + $includeExpensive = $input->getOption('include-expensive'); + if ($includeExpensive) { + foreach ($this->repair->getExpensiveRepairSteps() as $step) { + $this->repair->addStep($step); + } + } - $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']); + //add all possible steps and run + foreach ($appSteps as $step) { + $this->repair->addStep($step); + } + $this->initializeProgressBar($output); $this->repair->run(); - - $this->config->setSystemValue('maintenance', $maintenanceMode); + return 0; } public function handleRepairFeedBack($event) { @@ -131,10 +174,10 @@ public function handleRepairFeedBack($event) { $this->output->writeln(''); break; case '\OC\Repair::step': - $this->output->writeln(' - ' . $event->getArgument(0)); + $this->output->writeln(' - Step: ' . $event->getArgument(0)); break; case '\OC\Repair::info': - $this->output->writeln(' - ' . $event->getArgument(0)); + $this->output->writeln(' - INFO: ' . $event->getArgument(0)); break; case '\OC\Repair::warning': $this->output->writeln(' - WARNING: ' . $event->getArgument(0)); @@ -144,4 +187,46 @@ public function handleRepairFeedBack($event) { break; } } + + /** + * @param OutputInterface $output + * @@return IRepairStep[] + */ + public function getAppsRepairSteps(OutputInterface $output) { + $apps = $this->appManager->getEnabledAppsForUser(); + $steps = []; + foreach ($apps as $app) { + try { + $info = $this->appManager->getAppInfo($app); + if (!\is_array($info)) { + continue; + } + \OC_App::loadApp($app); + $appSteps = $info['repair-steps']['post-migration']; + foreach ($appSteps as $step) { + \array_push($steps, \OC::$server->query($step)); + } + } catch (\OC\NeedsUpdateException $ex) { + $output->writeln("ownCloud or one of the apps require upgrade."); + } catch (Exception $ex) { + $output->writeln("Failed to load repair step for $app: {$ex->getMessage()}"); + } + } + return $steps; + } + + /** + * @param OutputInterface $output + */ + protected function initializeProgressBar(OutputInterface $output) { + $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']); + } } diff --git a/core/register_command.php b/core/register_command.php index f50d4a06f16a..a2b544c822bf 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -133,8 +133,11 @@ $application->add(new OC\Core\Command\Upgrade(\OC::$server->getConfig(), \OC::$server->getLogger())); $application->add(new OC\Core\Command\Maintenance\Repair( - new \OC\Repair(\OC\Repair::getRepairSteps(), \OC::$server->getEventDispatcher()), \OC::$server->getConfig(), - \OC::$server->getEventDispatcher())); + new \OC\Repair(\OC\Repair::getRepairSteps(), \OC::$server->getEventDispatcher()), + \OC::$server->getConfig(), + \OC::$server->getEventDispatcher(), + \OC::$server->getAppManager()) + ); $application->add(new OC\Core\Command\User\Add(\OC::$server->getUserManager(), \OC::$server->getGroupManager(), \OC::$server->getMailer())); $application->add(new OC\Core\Command\User\Delete(\OC::$server->getUserManager())); diff --git a/lib/private/Repair.php b/lib/private/Repair.php index 371b7ee8b03d..f9cb6dacdefd 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -29,7 +29,6 @@ namespace OC; -use OC\Repair\Apps; use OC\Repair\CleanTags; use OC\Repair\Collation; use OC\Repair\DisableExtraThemes; @@ -52,14 +51,14 @@ use OCP\AppFramework\QueryException; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; -use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; use OC\Repair\MoveAvatarOutsideHome; class Repair implements IOutput { /* @var IRepairStep[] */ private $repairSteps; - /** @var EventDispatcher */ + /** @var EventDispatcherInterface */ private $dispatcher; /** @var string */ private $currentStep; @@ -68,9 +67,9 @@ class Repair implements IOutput { * Creates a new repair step runner * * @param IRepairStep[] $repairSteps array of RepairStep instances - * @param EventDispatcher $dispatcher + * @param EventDispatcherInterface $dispatcher */ - public function __construct($repairSteps = [], EventDispatcher $dispatcher = null) { + public function __construct($repairSteps = [], EventDispatcherInterface $dispatcher = null) { $this->repairSteps = $repairSteps; $this->dispatcher = $dispatcher; } diff --git a/lib/private/Repair/MoveAvatarOutsideHome.php b/lib/private/Repair/MoveAvatarOutsideHome.php index 8725f4a22510..1d2ff67616ac 100644 --- a/lib/private/Repair/MoveAvatarOutsideHome.php +++ b/lib/private/Repair/MoveAvatarOutsideHome.php @@ -141,6 +141,8 @@ public function run(IOutput $output) { $this->userManager->callForSeenUsers($function); $output->finishProgress(); + } else { + $output->info("No action required"); } } } diff --git a/tests/Core/Command/Maintenance/RepairTest.php b/tests/Core/Command/Maintenance/RepairTest.php new file mode 100644 index 000000000000..37b318f61eb8 --- /dev/null +++ b/tests/Core/Command/Maintenance/RepairTest.php @@ -0,0 +1,87 @@ + + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @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 + * + */ + +namespace Tests\Core\Command\User; + +use OC\Core\Command\Maintenance\Repair; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Tester\CommandTester; +use Test\TestCase; + +/** + * Class RepairTest + * + * @group DB + */ +class RepairTest extends TestCase { + + /** @var CommandTester */ + private $commandTester; + + protected function setUp() { + parent::setUp(); + + $application = new Application( + \OC::$server->getConfig(), + \OC::$server->getEventDispatcher(), + \OC::$server->getRequest() + ); + $command = new Repair( + new \OC\Repair(\OC\Repair::getRepairSteps(), \OC::$server->getEventDispatcher()), + \OC::$server->getConfig(), + \OC::$server->getEventDispatcher(), + \OC::$server->getAppManager() + ); + $command->setApplication($application); + $this->commandTester = new CommandTester($command); + } + + protected function tearDown() { + parent::tearDown(); + \OC::$server->getConfig()->setSystemValue('maintenance', false); + } + + /** + * @dataProvider inputProvider + * @param array $input + * @param boolean $maintenanceMode + * @param integer $returnValue + * @param string $expectedOutput + */ + public function testCommandInput($input, $maintenanceMode, $returnValue, $expectedOutput) { + \OC::$server->getConfig()->setSystemValue('maintenance', $maintenanceMode); + $result = $this->commandTester->execute($input); + $this->assertEquals($result, $returnValue); + $output = $this->commandTester->getDisplay(); + $this->assertContains($expectedOutput, $output); + } + + public function inputProvider() { + return [ + [['--list' => true], true, 0, 'Found'], + [[], false, 0, 'Turn on maintenance mode to use this command'], + [['--single' => '\OC\UnexistingClass'], true, 1, 'Repair step not found'], + [['--single' => 'OC\Repair\RepairMimeTypes'], true, 0, 'Repair mime types'], + [[], true, 0, '100%'], + [['--include-expensive' => true], true, 0, 'Remove shares of old group memberships'] + ]; + } +}