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');
- }
-}