From 3b7dbf23cceed4c7edcc719441240517df1153bd Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Mon, 9 Sep 2024 23:49:04 -0100 Subject: [PATCH] migrating custom groups Signed-off-by: Maxence Lange d Signed-off-by: Maxence Lange --- appinfo/info.xml | 2 + lib/Command/MigrateCustomGroups.php | 256 +++++++++++++++++++ lib/Migration/ImportOwncloudCustomGroups.php | 209 --------------- 3 files changed, 258 insertions(+), 209 deletions(-) create mode 100644 lib/Command/MigrateCustomGroups.php delete mode 100644 lib/Migration/ImportOwncloudCustomGroups.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 56fbd4834..97ce852b5 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -70,6 +70,8 @@ Those groups of people can then be used by any other app for sharing purpose. OCA\Circles\Command\MembersDetails OCA\Circles\Command\MembersLevel OCA\Circles\Command\MembersRemove + + OCA\Circles\Command\MigrateCustomGroups diff --git a/lib/Command/MigrateCustomGroups.php b/lib/Command/MigrateCustomGroups.php new file mode 100644 index 000000000..7d293e1a3 --- /dev/null +++ b/lib/Command/MigrateCustomGroups.php @@ -0,0 +1,256 @@ +setName('circles:migrate:customgroups'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->output = $output; + if (!$this->shouldRun()) { + $this->output->writeln('migration already done or table \'custom_group\' not found'); + return 0; + } + + $this->migrateTeams(); + $this->config->setAppValue('circles', 'imported_custom_groups', 'true'); + + return 0; + } + + public function migrateTeams(): void { + $this->output->writeln('Migrating custom groups to Teams'); + + $owners = $this->extractCustomGroupsAndOwners(); + + // we get list of all custom groups + $queryCustomGroups = $this->connection->getQueryBuilder(); + $queryCustomGroups->select('group_id', 'display_name', 'uri') + ->from('custom_group') + ->orderBy('group_id'); + + $resultCustomGroups = $queryCustomGroups->executeQuery(); + + // we cycle for each custom group + while ($rowCG = $resultCustomGroups->fetch()) { + $groupId = $rowCG['group_id'] ?? 0; + $groupUri = $rowCG['uri'] ?? ''; + $ownerId = $owners[$groupId] ?? ''; + if ($ownerId === '' || $groupId === 0) { + continue; // if group or owner is not know, we ignore the entry. + } + + // based on owner's userid, we create federateduser and a new circle + $this->output->writeln('+ New Team ' . $rowCG['display_name'] . ', owner by ' . $ownerId . ''); + $owner = $this->cachedFed($ownerId); + + $this->circlesManager->startSession($owner); + $circle = $this->circlesManager->createCircle($rowCG['display_name']); + + // we get all members for this custom group + $queryMembers = $this->connection->getQueryBuilder(); + $queryMembers->select('user_id', 'role') + ->from('custom_group_member') + ->where($queryMembers->expr()->eq('group_id', $queryMembers->createNamedParameter($groupId))); + + $members = [$ownerId]; + $resultMembers = $queryMembers->executeQuery(); + while ($rowM = $resultMembers->fetch()) { + $userId = $rowM['user_id']; + // if admin, ignore + if ($userId === '') { + continue; + } + + try { + $members[] = $userId; + if ($userId === $ownerId) { + continue; // owner is already in the circles + } + + $this->output->writeln(' - new member ' . $userId .''); + $member = $this->circlesManager->addMember($circle->getSingleId(), $this->cachedFed($userId)); + if ($rowM['role'] === '1') { + $this->circlesManager->levelMember($member->getId(), Member::LEVEL_ADMIN); + } + } catch (\Exception $e) { + $this->output->writeln('' . get_class($e) . ' ' . $e->getMessage() . ''); + $this->logger->log(2, 'error while migrating custom group member', ['exception' => $e]); + } + } + + $this->circlesManager->stopSession(); + $resultMembers->closeCursor(); + + $this->updateShares($groupUri, $circle->getSingleId(), $members); + $this->output->writeln(''); + } + + $resultCustomGroups->closeCursor(); + } + + /** + * - type 7 instead of 1 + * - with circle ID instead of `customgroup_` + group URI + * - update children using memberIds + * + * @param string $groupUri + * @param string $circleId + * @param array $memberIds + * + * @throws Exception + */ + public function updateShares(string $groupUri, string $circleId, array $memberIds): void { + $shareIds = $this->getSharedIds($groupUri); + + $update = $this->connection->getQueryBuilder(); + $update->update('share') + ->set('share_type', $update->createNamedParameter(IShare::TYPE_CIRCLE)) + ->set('share_with', $update->createNamedParameter($circleId)) + ->where($update->expr()->in('id', $update->createNamedParameter($shareIds, IQueryBuilder::PARAM_INT_ARRAY))); + + $count = $update->executeStatement(); + $this->output->writeln('> ' . $count . ' shares updated'); + + $this->fixShareChildren($shareIds, $memberIds); + } + + /** + * manage local cache FederatedUser + * + * @param string $userId + * @return FederatedUser + */ + private function cachedFed(string $userId): FederatedUser { + if (!array_key_exists($userId, $this->fedList)) { + $this->fedList[$userId] = $this->circlesManager->getLocalFederatedUser($userId); + } + + return $this->fedList[$userId]; + } + + /** + * update share children using the correct member id + * + * @param string $shareId + * @param array $memberIds + */ + private function fixShareChildren(array $shareIds, array $memberIds): void { + $update = $this->connection->getQueryBuilder(); + $update->update('share') + ->set('share_type', $update->createNamedParameter(IShare::TYPE_CIRCLE)) + ->set('share_with', $update->createParameter('new_recipient')) + ->where($update->expr()->in('parent', $update->createNamedParameter($shareIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($update->expr()->eq('share_with', $update->createParameter('old_recipient'))); + + $count = 0; + foreach($memberIds as $memberId) { + $update->setParameter('old_recipient', $memberId); + $update->setParameter('new_recipient', $this->cachedFed($memberId)->getSingleId()); + $count += $update->executeStatement(); + } + + $this->output->writeln('> ' . $count . ' children shares updated'); + } + + + private function getSharedIds(string $groupUri): array { + $select = $this->connection->getQueryBuilder(); + $select->select('*') + ->from('share') + ->where($select->expr()->eq('share_type', $select->createNamedParameter(IShare::TYPE_GROUP))); + + $shareIds = []; + $result = $select->execute(); + while ($row = $result->fetch()) { + $with = $row['share_with']; + if (!str_starts_with($with, 'customgroup_') + || substr($with, strlen('customgroup_')) !== $groupUri) { + // not a custom group, or not the one we're looking for + continue; + } + + $shareIds[] = $row['id']; + } + + return $shareIds; + } + + protected function shouldRun(): bool { + $alreadyImported = $this->config->getAppValue('circles', 'imported_custom_groups', 'false'); + return $alreadyImported === 'false' && $this->connection->tableExists('custom_group') && $this->connection->tableExists('custom_group_member'); + } + + /** + * returns owners for each custom groups + * + * @return array [groupId => userId] + * @throws Exception + */ + private function extractCustomGroupsAndOwners(): array { + $queryOwners = $this->connection->getQueryBuilder(); + $queryOwners->select('group_id', 'user_id') + ->from('custom_group_member') + ->where($queryOwners->expr()->eq('role', $queryOwners->createNamedParameter('1'))); + + $resultOwners = $queryOwners->executeQuery(); + $owners = []; + while ($rowO = $resultOwners->fetch()) { + // no idea if custom groups in owncloud can hold multiple 'owner' + $owners[$rowO['group_id']] = $owners[$rowO['group_id']] ?? $rowO['user_id']; + } + $resultOwners->closeCursor(); + + return $owners; + } +} diff --git a/lib/Migration/ImportOwncloudCustomGroups.php b/lib/Migration/ImportOwncloudCustomGroups.php deleted file mode 100644 index cd0ef3dba..000000000 --- a/lib/Migration/ImportOwncloudCustomGroups.php +++ /dev/null @@ -1,209 +0,0 @@ -connection = $connection; - $this->config = $config; - } - - /** - * Returns the step's name - * - * @return string - * @since 9.1.0 - */ - public function getName() { - return 'Fix the share type of guest shares when migrating from ownCloud'; - } - - /** - * @param IOutput $output - */ - public function run(IOutput $output) { - if (!$this->shouldRun()) { - return; - } - - $this->createCircles($output); - $this->createMemberships($output); - $this->updateShares($output); - - $this->config->setAppValue('circles', 'imported_custom_groups', 'true'); - } - - /** - * @param IOutput $output - */ - public function createCircles(IOutput $output) { - $output->info('Creating circles'); - - $select = $this->connection->getQueryBuilder(); - $select->select('*') - ->from('custom_group') - ->orderBy('group_id'); - - $insert = $this->connection->getQueryBuilder(); - $insert->insert('circle_circles') - ->values([ - 'name' => $insert->createParameter('name'), - 'type' => $insert->createParameter('type'), - 'creation' => $insert->createFunction('NOW()'), - ]); - - $output->startProgress(); - $result = $select->execute(); - - while ($row = $result->fetch()) { - $insert->setParameter('name', $row['display_name']) - ->setParameter('type', DeprecatedCircle::CIRCLES_CLOSED); - - $insert->execute(); - $output->advance(); - - $this->circlesById[$row['groud_id']] = $insert->getLastInsertId(); - $this->circlesByUri[$row['uri']] = $this->circlesById[$row['groud_id']]; - } - - $result->closeCursor(); - $output->finishProgress(); - } - - /** - * @param IOutput $output - */ - public function createMemberships(IOutput $output) { - $output->info('Creating memberships'); - - $select = $this->connection->getQueryBuilder(); - $select->select('*') - ->from('custom_group_member') - ->orderBy('group_id'); - - $insert = $this->connection->getQueryBuilder(); - $insert->insert('circle_members') - ->values([ - 'circle_id' => $insert->createParameter('circle_id'), - 'user_id' => $insert->createParameter('user_id'), - 'level' => $insert->createParameter('level'), - 'status' => $insert->createParameter('status'), - 'joined' => $insert->createFunction('NOW()'), - ]); - - $output->startProgress(); - $result = $select->execute(); - - while ($row = $result->fetch()) { - if (!isset($this->circlesById[$row['group_id']])) { - // Stray membership - continue; - } - - $level = (int) $row['role'] === 1 ? DeprecatedMember::LEVEL_OWNER : DeprecatedMember::LEVEL_MEMBER; - - if ($level === DeprecatedMember::LEVEL_OWNER) { - if (isset($this->circleHasAdmin[$this->circlesById[$row['group_id']]])) { - $level = DeprecatedMember::LEVEL_MODERATOR; - } else { - $this->circleHasAdmin[$this->circlesById[$row['group_id']]] = $row['user_id']; - } - } - - $insert->setParameter('circle_id', $this->circlesById[$row['group_id']]) - ->setParameter('user_id', $row['user_id']) - ->setParameter('level', $level) - ->setParameter('status', 'Member'); - - $insert->execute(); - $output->advance(); - } - - $result->closeCursor(); - $output->finishProgress(); - } - - /** - * Update shares - * - type 7 instead of 1 - * - with circle ID instead of `customgroup_` + group URI - * - * @param IOutput $output - */ - public function updateShares(IOutput $output) { - $output->info('Update shares from custom groups to circles'); - - $select = $this->connection->getQueryBuilder(); - $select->select('*') - ->from('share') - ->where($select->expr()->eq('share_type', $select->createNamedParameter(Share::SHARE_TYPE_GROUP))); - - $update = $this->connection->getQueryBuilder(); - $update->update('share') - ->set('share_type', $update->createParameter('type')) - ->set('share_with', $update->createParameter('with')) - ->where($update->expr()->eq('id', $update->createParameter('id'))); - - $output->startProgress(); - $result = $select->execute(); - - while ($row = $result->fetch()) { - $with = $row['share_with']; - if (strpos($with, 'customgroup_') !== 0) { - // Stray membership - continue; - } - - $groupUri = substr($with, strlen('customgroup_')); - if ($groupUri === '' || !isset($this->circlesByUri[$groupUri])) { - // Not a customgroup - continue; - } - - $update->setParameter('type', Share::SHARE_TYPE_CIRCLE) - ->setParameter('with', $this->circlesByUri[$groupUri]) - ->setParameter('id', $row['id']); - - $update->execute(); - $output->advance(); - } - - $result->closeCursor(); - $output->finishProgress(); - } - - protected function shouldRun() { - $alreadyImported = $this->config->getAppValue('circles', 'imported_custom_groups', 'false'); - return !$alreadyImported && $this->connection->tableExists('custom_group') && $this->connection->tableExists('custom_group_member'); - } -}