*/
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.
}
$name = $rowCG['display_name'];
while (strlen($name) < 3) {
$name = '_' . $name;
}
$this->output->writeln('+ New Team ' . $name . ', owned by ' . $ownerId . '');
// based on owner's userid, we create federateduser and a new circle
$owner = $this->cachedFed($ownerId);
if ($owner === null) {
$this->output->writeln('unknown user ' . $ownerId);
continue;
}
$this->circlesManager->startSession($owner);
try {
$circle = $this->circlesManager->createCircle($name);
} catch (\Exception $e) {
$this->output->writeln('' . get_class($e) . ' ' . $e->getMessage() . ' with data ' . json_encode($rowCG));
$this->logger->log(2, 'error while creating team', ['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
}
$fedUser = $this->cachedFed($userId);
if ($fedUser === null) {
$this->output->writeln('unknown user ' . $userId);
continue;
}
$this->output->writeln(' - new member ' . $userId . '');
$member = $this->circlesManager->addMember($circle->getSingleId(), $fedUser);
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('> ' . ((string)$count) . ' shares updated');
$this->fixShareChildren($shareIds, $memberIds);
}
/**
* manage local cache FederatedUser
*
* @param string $userId
* @return null|FederatedUser
*/
private function cachedFed(string $userId): ?FederatedUser {
if (!array_key_exists($userId, $this->fedList)) {
try {
$this->fedList[$userId] = $this->circlesManager->getLocalFederatedUser($userId);
} catch (\Exception $e) {
$this->logger->warning('unknown local user ' . $userId, ['exception' => $e]);
$this->fedList[$userId] = null;
}
}
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) {
$fedUser = $this->cachedFed($memberId);
if ($fedUser === null) {
// we dont update, user does not exist anymore
continue;
}
$update->setParameter('old_recipient', $memberId);
$update->setParameter('new_recipient', $fedUser->getSingleId());
$count += $update->executeStatement();
}
$this->output->writeln('> ' . ((string)$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;
}
}