670 lines
17 KiB
PHP
670 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2021 F7cloud GmbH and F7cloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
|
|
namespace OCA\Circles\Service;
|
|
|
|
use Exception;
|
|
use OCA\Circles\AppInfo\Application;
|
|
use OCA\Circles\Db\CircleRequest;
|
|
use OCA\Circles\Db\MemberRequest;
|
|
use OCA\Circles\Db\ShareTokenRequest;
|
|
use OCA\Circles\Exceptions\CircleNotFoundException;
|
|
use OCA\Circles\Exceptions\ContactAddressBookNotFoundException;
|
|
use OCA\Circles\Exceptions\ContactFormatException;
|
|
use OCA\Circles\Exceptions\ContactNotFoundException;
|
|
use OCA\Circles\Exceptions\FederatedItemException;
|
|
use OCA\Circles\Exceptions\FederatedUserException;
|
|
use OCA\Circles\Exceptions\FederatedUserNotFoundException;
|
|
use OCA\Circles\Exceptions\InvalidIdException;
|
|
use OCA\Circles\Exceptions\MemberNotFoundException;
|
|
use OCA\Circles\Exceptions\MigrationException;
|
|
use OCA\Circles\Exceptions\OwnerNotFoundException;
|
|
use OCA\Circles\Exceptions\RemoteInstanceException;
|
|
use OCA\Circles\Exceptions\RemoteNotFoundException;
|
|
use OCA\Circles\Exceptions\RemoteResourceNotFoundException;
|
|
use OCA\Circles\Exceptions\RequestBuilderException;
|
|
use OCA\Circles\Exceptions\ShareTokenNotFoundException;
|
|
use OCA\Circles\Exceptions\SingleCircleNotFoundException;
|
|
use OCA\Circles\Exceptions\UnknownRemoteException;
|
|
use OCA\Circles\Exceptions\UserTypeNotFoundException;
|
|
use OCA\Circles\Model\Circle;
|
|
use OCA\Circles\Model\FederatedUser;
|
|
use OCA\Circles\Model\Member;
|
|
use OCA\Circles\Model\Probes\CircleProbe;
|
|
use OCA\Circles\Model\ShareToken;
|
|
use OCA\Circles\Tools\Model\SimpleDataStore;
|
|
use OCA\Circles\Tools\Traits\TNCLogger;
|
|
use OCA\Circles\Tools\Traits\TStringTools;
|
|
use OCA\DAV\CardDAV\ContactsManager;
|
|
use OCP\Contacts\IManager;
|
|
use OCP\IDBConnection;
|
|
use OCP\IURLGenerator;
|
|
use OCP\Server;
|
|
use OCP\Share\IShare;
|
|
|
|
/**
|
|
* Class MigrationService
|
|
*
|
|
* @package OCA\Circles\Service
|
|
*/
|
|
class MigrationService {
|
|
use TStringTools;
|
|
use TNCLogger;
|
|
|
|
|
|
/** @var IDBConnection */
|
|
private $dbConnection;
|
|
|
|
/** @var IURLGenerator */
|
|
private $urlGenerator;
|
|
|
|
/** @var CircleRequest */
|
|
private $circleRequest;
|
|
|
|
/** @var MemberRequest */
|
|
private $memberRequest;
|
|
|
|
/** @var ShareTokenRequest */
|
|
private $shareTokenRequest;
|
|
|
|
/** @var MembershipService */
|
|
private $membershipService;
|
|
|
|
/** @var FederatedUserService */
|
|
private $federatedUserService;
|
|
|
|
/** @var CircleService */
|
|
private $circleService;
|
|
|
|
/** @var ContactService */
|
|
private $contactService;
|
|
|
|
/** @var TimezoneService */
|
|
private $timezoneService;
|
|
|
|
/** @var OutputService */
|
|
private $outputService;
|
|
|
|
/** @var ConfigService */
|
|
private $configService;
|
|
|
|
|
|
/** @var FederatedUser */
|
|
private $appCircle = null;
|
|
|
|
|
|
/**
|
|
* MigrationService constructor.
|
|
*
|
|
* @param IDBConnection $dbConnection
|
|
* @param IURLGenerator $urlGenerator
|
|
* @param CircleRequest $circleRequest
|
|
* @param MemberRequest $memberRequest
|
|
* @param ShareTokenRequest $shareTokenRequest
|
|
* @param MembershipService $membershipService
|
|
* @param FederatedUserService $federatedUserService
|
|
* @param CircleService $circleService
|
|
* @param ContactService $contactService
|
|
* @param TimezoneService $timezoneService
|
|
* @param OutputService $outputService
|
|
* @param ConfigService $configService
|
|
*/
|
|
public function __construct(
|
|
IDBConnection $dbConnection,
|
|
IURLGenerator $urlGenerator,
|
|
CircleRequest $circleRequest,
|
|
MemberRequest $memberRequest,
|
|
ShareTokenRequest $shareTokenRequest,
|
|
MembershipService $membershipService,
|
|
FederatedUserService $federatedUserService,
|
|
CircleService $circleService,
|
|
ContactService $contactService,
|
|
TimezoneService $timezoneService,
|
|
OutputService $outputService,
|
|
ConfigService $configService,
|
|
) {
|
|
$this->dbConnection = $dbConnection;
|
|
$this->urlGenerator = $urlGenerator;
|
|
$this->circleRequest = $circleRequest;
|
|
$this->memberRequest = $memberRequest;
|
|
$this->shareTokenRequest = $shareTokenRequest;
|
|
$this->membershipService = $membershipService;
|
|
$this->federatedUserService = $federatedUserService;
|
|
$this->circleService = $circleService;
|
|
$this->contactService = $contactService;
|
|
$this->timezoneService = $timezoneService;
|
|
$this->outputService = $outputService;
|
|
$this->configService = $configService;
|
|
|
|
$this->setup('app', Application::APP_ID);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param bool $force
|
|
*
|
|
* @throws ContactAddressBookNotFoundException
|
|
* @throws ContactFormatException
|
|
* @throws ContactNotFoundException
|
|
* @throws FederatedUserException
|
|
* @throws InvalidIdException
|
|
* @throws MigrationException
|
|
* @throws RequestBuilderException
|
|
* @throws SingleCircleNotFoundException
|
|
*/
|
|
public function migration(bool $force = false): void {
|
|
if ($this->configService->getAppValueBool(ConfigService::MIGRATION_RUN)) {
|
|
throw new MigrationException('A migration process is already running');
|
|
}
|
|
$this->configService->setAppValue(ConfigService::MIGRATION_RUN, '1');
|
|
|
|
if ($force) {
|
|
$this->configService->setAppValue(ConfigService::MIGRATION_22, '0');
|
|
$this->configService->setAppValue(ConfigService::MIGRATION_22_1, '0');
|
|
// $this->configService->setAppValue(ConfigService::MIGRATION_23, '0');
|
|
}
|
|
|
|
$this->appCircle = $this->federatedUserService->getAppInitiator(
|
|
Application::APP_ID,
|
|
Member::APP_CIRCLES
|
|
);
|
|
|
|
$this->migrationTo22();
|
|
|
|
$this->configService->setAppValue(ConfigService::MIGRATION_RUN, '0');
|
|
}
|
|
|
|
|
|
/**
|
|
* @throws RequestBuilderException
|
|
*/
|
|
private function migrationTo22(): void {
|
|
if ($this->configService->getAppValueBool(ConfigService::MIGRATION_22)) {
|
|
return;
|
|
}
|
|
|
|
if (!$this->migrationTo22Feasibility()) {
|
|
$this->configService->setAppValue(ConfigService::MIGRATION_22, '1');
|
|
|
|
return;
|
|
}
|
|
|
|
$this->outputService->output('Migrating to 22');
|
|
|
|
$this->migrationTo22_Circles();
|
|
$this->migrationTo22_Members();
|
|
$this->membershipService->resetMemberships('', true);
|
|
$this->migrationTo22_Members_Memberships();
|
|
|
|
$this->migrationTo22_Tokens();
|
|
$this->migrationTo22_1_SubShares();
|
|
|
|
$this->configService->setAppValue(ConfigService::MIGRATION_22, '1');
|
|
}
|
|
|
|
|
|
/**
|
|
* run migration if:
|
|
* - old tables exist.
|
|
* - new tables are (almost) empty.
|
|
*
|
|
* @return bool
|
|
* @throws \OCP\DB\Exception
|
|
*/
|
|
public function migrationTo22Feasibility(): bool {
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$qb->select('*')->from('circle_circles');
|
|
|
|
try {
|
|
$cursor = $qb->executeQuery();
|
|
$cursor->closeCursor();
|
|
} catch (\OCP\DB\Exception $e) {
|
|
return false;
|
|
}
|
|
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$qb->select('*')->from('circles_circle');
|
|
|
|
$cursor = $qb->executeQuery();
|
|
if ($cursor->rowCount() > 1) {
|
|
return false;
|
|
}
|
|
$cursor->closeCursor();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @throws RequestBuilderException
|
|
*/
|
|
public function migrationTo22_Members_Memberships(): void {
|
|
$probe = new CircleProbe();
|
|
$probe->includeSystemCircles();
|
|
$circles = $this->circleRequest->getCircles(null, $probe);
|
|
|
|
$this->outputService->startMigrationProgress(sizeof($circles));
|
|
|
|
$done = [];
|
|
foreach ($circles as $circle) {
|
|
$this->outputService->output(
|
|
'Caching memberships for Members of \'' . $circle->getDisplayName() . '\'',
|
|
true
|
|
);
|
|
|
|
$members = $circle->getMembers();
|
|
foreach ($members as $member) {
|
|
if (in_array($member->getSingleId(), $done)) {
|
|
continue;
|
|
}
|
|
|
|
$this->membershipService->manageMemberships($member->getSingleId());
|
|
$done[] = $member->getSingleId();
|
|
}
|
|
}
|
|
|
|
$this->outputService->finishMigrationProgress();
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
*/
|
|
private function migrationTo22_Circles(): void {
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$qb->select('*')->from('circle_circles');
|
|
|
|
try {
|
|
$cursor = $qb->executeQuery();
|
|
$this->outputService->startMigrationProgress($cursor->rowCount());
|
|
|
|
while ($row = $cursor->fetch()) {
|
|
try {
|
|
$data = new SimpleDataStore($row);
|
|
$this->outputService->output(
|
|
'Migrating Circle \'' . $data->g('name') . '\' (' . $data->g('unique_id') . ')',
|
|
true
|
|
);
|
|
|
|
$circle = $this->generateCircleFrom21($data);
|
|
$this->saveGeneratedCircle($circle);
|
|
} catch (Exception $e) {
|
|
}
|
|
}
|
|
|
|
$cursor->closeCursor();
|
|
} catch (\OCP\DB\Exception $e) {
|
|
}
|
|
|
|
$this->outputService->finishMigrationProgress();
|
|
}
|
|
|
|
|
|
/**
|
|
* @param SimpleDataStore $data
|
|
*
|
|
* @return Circle
|
|
* @throws RequestBuilderException
|
|
*/
|
|
private function generateCircleFrom21(SimpleDataStore $data): Circle {
|
|
$circle = new Circle();
|
|
$circle->setSingleId($data->g('unique_id'))
|
|
->setName($data->g('name'))
|
|
->setDisplayName($data->g('display_name'))
|
|
->setSettings($data->gArray('settings'))
|
|
->setDescription($data->g('description'))
|
|
->setContactAddressBook($data->gInt('contact_addressbook'))
|
|
->setContactGroupName($data->g('contact_groupname'))
|
|
->setSource(Member::TYPE_CIRCLE);
|
|
|
|
$dTime = $this->timezoneService->getDateTime($data->g('creation'));
|
|
$circle->setCreation($dTime->getTimestamp());
|
|
|
|
if ($circle->getDisplayName() === '') {
|
|
$circle->setDisplayName($circle->getName());
|
|
}
|
|
|
|
$this->circleService->generateSanitizedName($circle);
|
|
$this->convertCircleTypeFrom21($circle, $data->gInt('type'));
|
|
|
|
return $circle;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param Circle $circle
|
|
* @param int $type
|
|
*/
|
|
private function convertCircleTypeFrom21(Circle $circle, int $type): void {
|
|
switch ($type) {
|
|
case 1: // personal
|
|
$circle->setConfig(Circle::CFG_PERSONAL);
|
|
break;
|
|
|
|
case 2: // secret
|
|
$circle->setConfig(Circle::CFG_OPEN + Circle::CFG_REQUEST);
|
|
break;
|
|
|
|
case 4: // closed
|
|
$circle->setConfig(Circle::CFG_OPEN + Circle::CFG_REQUEST + Circle::CFG_VISIBLE);
|
|
break;
|
|
|
|
case 8: // public
|
|
$circle->setConfig(Circle::CFG_OPEN + Circle::CFG_VISIBLE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param Circle $circle
|
|
*/
|
|
private function saveGeneratedCircle(Circle $circle): void {
|
|
try {
|
|
$this->circleRequest->getCircle($circle->getSingleId());
|
|
} catch (CircleNotFoundException $e) {
|
|
try {
|
|
$this->circleRequest->save($circle);
|
|
} catch (InvalidIdException $e) {
|
|
}
|
|
} catch (RequestBuilderException $e) {
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
*/
|
|
private function migrationTo22_Members(): void {
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$qb->select('*')->from('circle_members');
|
|
|
|
try {
|
|
$cursor = $qb->executeQuery();
|
|
$this->outputService->startMigrationProgress($cursor->rowCount());
|
|
|
|
while ($row = $cursor->fetch()) {
|
|
try {
|
|
$data = new SimpleDataStore($row);
|
|
$this->outputService->output(
|
|
'Migrating Member \'' . $data->g('user_id') . '\' from \'' . $data->g('circle_id')
|
|
. '\'',
|
|
true
|
|
);
|
|
|
|
$member = $this->generateMemberFrom21($data);
|
|
$this->saveGeneratedMember($member);
|
|
} catch (Exception $e) {
|
|
}
|
|
}
|
|
|
|
$cursor->closeCursor();
|
|
} catch (\OCP\DB\Exception $e) {
|
|
}
|
|
|
|
$this->outputService->finishMigrationProgress();
|
|
}
|
|
|
|
|
|
/**
|
|
* @throws CircleNotFoundException
|
|
* @throws RemoteInstanceException
|
|
* @throws UserTypeNotFoundException
|
|
* @throws FederatedUserNotFoundException
|
|
* @throws OwnerNotFoundException
|
|
* @throws RequestBuilderException
|
|
* @throws RemoteNotFoundException
|
|
* @throws UnknownRemoteException
|
|
* @throws FederatedUserException
|
|
* @throws ContactAddressBookNotFoundException
|
|
* @throws RemoteResourceNotFoundException
|
|
* @throws MemberNotFoundException
|
|
* @throws FederatedItemException
|
|
* @throws SingleCircleNotFoundException
|
|
* @throws InvalidIdException
|
|
*/
|
|
private function generateMemberFrom21(SimpleDataStore $data): Member {
|
|
$member = new Member();
|
|
|
|
$member->setCircleId($data->g('circle_id'))
|
|
->setId($data->g('member_id'))
|
|
->setUserId($data->g('user_id'))
|
|
->setInstance($data->g('instance'))
|
|
->setDisplayName($data->g('cached_name'))
|
|
->setLevel($data->gInt('level'))
|
|
->setStatus($data->g('status'))
|
|
->setContactMeta($data->g('contact_meta'))
|
|
->setContactId($data->g('contact_id'))
|
|
->setInvitedBy($this->appCircle);
|
|
|
|
$this->convertMemberUserTypeFrom21($member, $data->gInt('user_type'));
|
|
|
|
$singleMember = $this->federatedUserService->getFederatedUser(
|
|
$member->getUserId(),
|
|
$member->getUserType()
|
|
);
|
|
|
|
$member->setSingleId($singleMember->getSingleId());
|
|
|
|
return $member;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param Member $member
|
|
* @param int $userType
|
|
*
|
|
* @throws ContactAddressBookNotFoundException
|
|
*/
|
|
private function convertMemberUserTypeFrom21(Member $member, int $userType): void {
|
|
switch ($userType) {
|
|
case 1:
|
|
$member->setUserType(1);
|
|
|
|
return;
|
|
case 2:
|
|
$member->setUserType(2);
|
|
|
|
return;
|
|
case 3:
|
|
$member->setUserType(4);
|
|
|
|
return;
|
|
case 4:
|
|
$member->setUserType(8);
|
|
$this->fixContactId($member);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
private function migrationTo22_1_SubShares(): void {
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$expr = $qb->expr();
|
|
$qb->select('*')
|
|
->from('share')
|
|
->where($expr->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)))
|
|
->andWhere($expr->isNotNull('parent'));
|
|
|
|
try {
|
|
$cursor = $qb->executeQuery();
|
|
$this->outputService->startMigrationProgress($cursor->rowCount());
|
|
|
|
while ($row = $cursor->fetch()) {
|
|
try {
|
|
$data = new SimpleDataStore($row);
|
|
$federatedUser =
|
|
$this->federatedUserService->getLocalFederatedUser($data->g('share_with'));
|
|
$this->outputService->output(
|
|
'Migrating child share #' . $data->gInt('id') . ' owner: ' . $data->g('share_with')
|
|
. ' -> ' . $federatedUser->getSingleId(),
|
|
true
|
|
);
|
|
|
|
$this->updateSubShare($data, $federatedUser);
|
|
} catch (Exception $e) {
|
|
}
|
|
}
|
|
|
|
$cursor->closeCursor();
|
|
} catch (\OCP\DB\Exception $e) {
|
|
}
|
|
|
|
$this->outputService->finishMigrationProgress();
|
|
}
|
|
|
|
|
|
/**
|
|
* @param SimpleDataStore $data
|
|
* @param FederatedUser $federatedUser
|
|
*
|
|
* @throws \OCP\DB\Exception
|
|
*/
|
|
private function updateSubShare(SimpleDataStore $data, FederatedUser $federatedUser): void {
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$expr = $qb->expr();
|
|
$qb->select('*')
|
|
->from('share')
|
|
->where($expr->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)))
|
|
->andWhere($expr->eq('parent', $qb->createNamedParameter($data->gInt('parent'))))
|
|
->andWhere($expr->eq('share_with', $qb->createNamedParameter($federatedUser->getSingleId())));
|
|
|
|
$cursor = $qb->executeQuery();
|
|
if ($cursor->rowCount() > 0) {
|
|
// TODO: delete current row ?
|
|
return;
|
|
}
|
|
$cursor->closeCursor();
|
|
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$expr = $qb->expr();
|
|
$qb->update('share')
|
|
->set('share_with', $qb->createNamedParameter($federatedUser->getSingleId()))
|
|
->where($expr->eq('share_type', $qb->createNamedParameter(IShare::TYPE_CIRCLE)))
|
|
->andWhere($expr->eq('id', $qb->createNamedParameter($data->gInt('id'))))
|
|
->setMaxResults(1);
|
|
|
|
$qb->executeStatement();
|
|
}
|
|
|
|
|
|
/**
|
|
* @param Member $member
|
|
*
|
|
* @throws ContactAddressBookNotFoundException
|
|
*/
|
|
private function fixContactId(Member $member) {
|
|
[$userId, $contactId] = explode(':', $member->getUserId());
|
|
|
|
$contactsManager = Server::get(ContactsManager::class);
|
|
|
|
$cm = Server::get(IManager::class);
|
|
$contactsManager->setupContactsProvider($cm, $userId, $this->urlGenerator);
|
|
|
|
$contact = $cm->search($contactId, ['UID']);
|
|
if (sizeof($contact) === 1) {
|
|
$entry = array_shift($contact);
|
|
$addressBook =
|
|
$this->contactService->getAddressBoxById($cm, $this->get('addressbook-key', $entry));
|
|
|
|
$member->setUserId($userId . '/' . $addressBook->getUri() . '/' . $contactId);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param Member $member
|
|
*/
|
|
private function saveGeneratedMember(Member $member): void {
|
|
try {
|
|
$this->memberRequest->getMemberById($member->getId());
|
|
} catch (MemberNotFoundException $e) {
|
|
try {
|
|
$this->memberRequest->save($member);
|
|
} catch (InvalidIdException $e) {
|
|
}
|
|
} catch (RequestBuilderException $e) {
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
*/
|
|
public function migrationTo22_Tokens(): void {
|
|
$qb = $this->dbConnection->getQueryBuilder();
|
|
$qb->select('*')->from('circle_tokens');
|
|
|
|
try {
|
|
$cursor = $qb->executeQuery();
|
|
$this->outputService->startMigrationProgress($cursor->rowCount());
|
|
|
|
while ($row = $cursor->fetch()) {
|
|
try {
|
|
$data = new SimpleDataStore($row);
|
|
$this->outputService->output(
|
|
'Migrating ShareToken \'' . $data->g('token') . '\' for \'' . $data->g('user_id')
|
|
. '\'',
|
|
true
|
|
);
|
|
|
|
$shareToken = $this->generateShareTokenFrom21($data);
|
|
$this->saveGeneratedShareToken($shareToken);
|
|
} catch (Exception $e) {
|
|
}
|
|
}
|
|
|
|
$cursor->closeCursor();
|
|
} catch (\OCP\DB\Exception $e) {
|
|
}
|
|
|
|
$this->outputService->finishMigrationProgress();
|
|
}
|
|
|
|
|
|
/**
|
|
* @param SimpleDataStore $data
|
|
*
|
|
* @return ShareToken
|
|
* @throws MemberNotFoundException
|
|
* @throws RequestBuilderException
|
|
*/
|
|
private function generateShareTokenFrom21(SimpleDataStore $data): ShareToken {
|
|
$shareToken = new ShareToken();
|
|
$member = $this->memberRequest->getMemberById($data->g('member_id'));
|
|
|
|
if ($member->getUserType() !== Member::TYPE_MAIL
|
|
&& $member->getUserType() !== Member::TYPE_CONTACT) {
|
|
throw new MemberNotFoundException();
|
|
}
|
|
|
|
$shareToken->setShareId($data->gInt('share_id'))
|
|
->setCircleId($data->g('circle_id'))
|
|
->setSingleId($member->getSingleId())
|
|
->setMemberId($data->g('member_id'))
|
|
->setToken($data->g('token'))
|
|
->setPassword($data->g('password'))
|
|
->setAccepted(IShare::STATUS_ACCEPTED);
|
|
|
|
return $shareToken;
|
|
}
|
|
|
|
/**
|
|
* @param ShareToken $shareToken
|
|
*/
|
|
private function saveGeneratedShareToken(ShareToken $shareToken): void {
|
|
try {
|
|
$this->shareTokenRequest->getByToken($shareToken->getToken());
|
|
} catch (ShareTokenNotFoundException $e) {
|
|
$this->shareTokenRequest->save($shareToken);
|
|
} catch (RequestBuilderException $e) {
|
|
}
|
|
}
|
|
}
|