Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

migrating custom groups #1685

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Those groups of people can then be used by any other app for sharing purpose.
<command>OCA\Circles\Command\MembersDetails</command>
<command>OCA\Circles\Command\MembersLevel</command>
<command>OCA\Circles\Command\MembersRemove</command>

<command>OCA\Circles\Command\MigrateCustomGroups</command>
</commands>

<activity>
Expand Down
249 changes: 249 additions & 0 deletions lib/Command/MigrateCustomGroups.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\Command;

use OC\Core\Command\Base;
use OCA\Circles\CirclesManager;
use OCA\Circles\IFederatedUser;
use OCA\Circles\Model\FederatedUser;
use OCA\Circles\Model\Member;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class MigrateCustomGroups extends Base {
private OutputInterface $output;
/** @var IFederatedUser[] */
private array $fedList = [];

public function __construct(
private CirclesManager $circlesManager,
protected IDBConnection $connection,
protected IConfig $config,
private LoggerInterface $logger,
) {
parent::__construct();
}

protected function configure() {
parent::configure();
$this->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 <info>' . $rowCG['display_name'] . '</info>, owner by <info>' . $ownerId . '</info>');
$owner = $this->cachedFed($ownerId);

$this->circlesManager->startSession($owner);
try {
$circle = $this->circlesManager->createCircle($rowCG['display_name']);
} catch (\Exception $e) {
$this->output->writeln('<error>' . get_class($e) . ' ' . $e->getMessage() . '</error> with data ' . json_encode($rowCG));
$this->logger->log(2, 'error while creating circle', ['exception' => $e]);
$this->circlesManager->stopSession();
continue;
}

// 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 <info>' . $userId .'</info>');
$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('<error>' . get_class($e) . ' ' . $e->getMessage() . '</error>');
$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<string, string> [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;
}
}
Loading
Loading