diff --git a/appinfo/info.xml b/appinfo/info.xml index 86fe64b6..ad8b87fe 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -46,6 +46,7 @@ This app allows users to easily migrate from one instance to another using an ex + OCA\UserMigration\Command\Manage OCA\UserMigration\Command\Export OCA\UserMigration\Command\Import diff --git a/lib/Command/Export.php b/lib/Command/Export.php index 018670a7..900f0352 100644 --- a/lib/Command/Export.php +++ b/lib/Command/Export.php @@ -10,8 +10,11 @@ namespace OCA\UserMigration\Command; use OC\Core\Command\Base; +use OCA\UserMigration\AppInfo\Application; use OCA\UserMigration\ExportDestination; use OCA\UserMigration\Service\UserMigrationService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; use OCP\IUser; use OCP\IUserManager; use OCP\UserMigration\IMigrator; @@ -26,6 +29,8 @@ class Export extends Base { public function __construct( private IUserManager $userManager, private UserMigrationService $migrationService, + private IConfig $config, + private ITimeFactory $timeFactory, ) { parent::__construct(); } @@ -173,6 +178,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (rename($path, $finalPath) === false) { throw new \Exception('Failed to rename ' . basename($path) . ' to ' . basename($finalPath)); } + $this->config->setUserValue($user->getUID(), Application::APP_ID, 'lastExport', (string)$this->timeFactory->getTime()); $io->writeln("Export saved in $finalPath"); } catch (\Exception $e) { if ($io->isDebug()) { diff --git a/lib/Command/Import.php b/lib/Command/Import.php index e4d9d353..28f474c1 100644 --- a/lib/Command/Import.php +++ b/lib/Command/Import.php @@ -9,8 +9,10 @@ namespace OCA\UserMigration\Command; +use OCA\UserMigration\AppInfo\Application; use OCA\UserMigration\ImportSource; use OCA\UserMigration\Service\UserMigrationService; +use OCP\IConfig; use OCP\IUserManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -23,6 +25,7 @@ class Import extends Command { public function __construct( private IUserManager $userManager, private UserMigrationService $migrationService, + private IConfig $config, ) { parent::__construct(); } @@ -73,6 +76,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->writeln("Importing from {$path}…"); $importSource = new ImportSource($path); $this->migrationService->import($importSource, $user, $io); + /* Reset exported state of user after import */ + $this->config->deleteUserValue($user->getUID(), Application::APP_ID, 'lastExport'); $io->writeln("Successfully imported from {$path}"); } catch (\Exception $e) { if ($io->isDebug()) { diff --git a/lib/Command/Manage.php b/lib/Command/Manage.php new file mode 100644 index 00000000..c37a1002 --- /dev/null +++ b/lib/Command/Manage.php @@ -0,0 +1,137 @@ +setName('user_migration:manage') + ->setDescription('List users exported by the admin, delete them by batch') + ->addOption( + 'limit', + 'l', + InputOption::VALUE_REQUIRED, + 'Limit the number of listed users', + 100, + ) + ->addOption( + 'since', + null, + InputOption::VALUE_REQUIRED, + 'Filter by minimum export date', + ) + ->addOption( + 'delete', + null, + InputOption::VALUE_NONE, + 'Delete the exported users', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + if ((string)$input->getOption('since') !== '') { + $since = new \DateTime($input->getOption('since')); + $output->writeln('Since ' . $since->format(\DateTimeInterface::ATOM) . ''); + } else { + $since = null; + } + $values = iterator_to_array($this->queryUsers((int)$input->getOption('limit'), $since)); + $this->writeTableInOutputFormat($input, $output, $values); + if ($input->getOption('delete')) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Please confirm to delete the above listed users [y/n]', !$input->isInteractive()); + + if (!$helper->ask($input, $output, $question)) { + $output->writeln('Deletion canceled'); + return self::SUCCESS; + } + $errors = $this->deleteUsers(array_column($values, 'userid'), $output); + if ($errors > 0) { + return self::FAILURE; + } + } + return self::SUCCESS; + } + + private function queryUsers(int $limit, ?\DateTime $since): \Generator { + $qb = $this->connection->getQueryBuilder(); + $qb->select('userid', 'configvalue') + ->from('preferences') + ->where($qb->expr()->eq('appid', $qb->createNamedParameter(Application::APP_ID))) + ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lastExport'))); + + if ($since !== null) { + $qb->andWhere($qb->expr()->gte('configvalue', $qb->createNamedParameter($since->getTimestamp(), IQueryBuilder::PARAM_INT))); + } + + $qb->orderBy('configvalue') + ->setMaxResults($limit); + + $result = $qb->executeQuery(); + + while ($row = $result->fetch()) { + yield [ + 'userid' => $row['userid'], + 'Last export' => date(\DateTimeInterface::ATOM, (int)$row['configvalue']) + ]; + } + + $result->closeCursor(); + } + + /** + * @param iterable $uids + */ + private function deleteUsers(iterable $uids, OutputInterface $output): int { + $errors = 0; + foreach ($uids as $uid) { + $user = $this->userManager->get($uid); + if (is_null($user)) { + $output->writeln('User ' . $uid . ' does not exist'); + $errors++; + continue; + } + + if ($user->delete()) { + $output->writeln('User "' . $uid . '" was deleted'); + } else { + $output->writeln('User "' . $uid . '" could not be deleted. Please check the logs.'); + $errors++; + } + } + return $errors; + } +} diff --git a/tests/stubs/stub.phpstub b/tests/stubs/stub.phpstub index 6a203ed5..c0eb2768 100644 --- a/tests/stubs/stub.phpstub +++ b/tests/stubs/stub.phpstub @@ -125,8 +125,9 @@ namespace OC\Cache { namespace OC\Core\Command { use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; + use Symfony\Component\Console\Command\Command; - class Base { + class Base extends Command { public const OUTPUT_FORMAT_PLAIN = 'plain'; public const OUTPUT_FORMAT_JSON = 'json'; public const OUTPUT_FORMAT_JSON_PRETTY = 'json_pretty'; @@ -139,5 +140,6 @@ namespace OC\Core\Command { public function setName(string $name) {} public function getHelper(string $name) {} protected function writeArrayInOutputFormat(InputInterface $input, OutputInterface $output, array $items, string $prefix = ' - ') {} + protected function writeTableInOutputFormat(InputInterface $input, OutputInterface $output, array $items): void {} } }