mirror of https://github.com/nextcloud/server.git
Add repair job to delete calendar subscriptions that were orphaned when
deleteding an user Follow-up to https://github.com/nextcloud/server/pull/28419 Signed-off-by: Thomas Citharel <tcit@tcit.fr>pull/28716/head
parent
113da79c8f
commit
45d5a797e7
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2021 Thomas Citharel <nextcloud@tcit.fr>
|
||||
*
|
||||
* @author Thomas Citharel <nextcloud@tcit.fr>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\DAV\Migration;
|
||||
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\IRepairStep;
|
||||
|
||||
class RemoveDeletedUsersCalendarSubscriptions implements IRepairStep {
|
||||
/** @var IDBConnection */
|
||||
private $connection;
|
||||
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
|
||||
/** @var int */
|
||||
private $progress = 0;
|
||||
|
||||
private $orphanSubscriptions = [];
|
||||
|
||||
private const SUBSCRIPTIONS_CHUNK_SIZE = 1000;
|
||||
|
||||
public function __construct(IDBConnection $connection, IUserManager $userManager) {
|
||||
$this->connection = $connection;
|
||||
$this->userManager = $userManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getName(): string {
|
||||
return 'Clean up old calendar subscriptions from deleted users that were not cleaned-up';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function run(IOutput $output) {
|
||||
$nbSubscriptions = $this->countSubscriptions();
|
||||
|
||||
$output->startProgress($nbSubscriptions);
|
||||
|
||||
while ($this->progress < $nbSubscriptions) {
|
||||
$this->checkSubscriptions();
|
||||
|
||||
$this->progress += self::SUBSCRIPTIONS_CHUNK_SIZE;
|
||||
$output->advance(min(self::SUBSCRIPTIONS_CHUNK_SIZE, $nbSubscriptions));
|
||||
}
|
||||
$output->finishProgress();
|
||||
$this->deleteOrphanSubscriptions();
|
||||
|
||||
$output->info(sprintf('%d calendar subscriptions without an user have been cleaned up', count($this->orphanSubscriptions)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function countSubscriptions(): int {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$query = $qb->select($qb->func()->count('*'))
|
||||
->from('calendarsubscriptions');
|
||||
|
||||
$result = $query->execute();
|
||||
$count = $result->fetchOne();
|
||||
$result->closeCursor();
|
||||
|
||||
if ($count !== false) {
|
||||
$count = (int)$count;
|
||||
} else {
|
||||
$count = 0;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function checkSubscriptions(): void {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$query = $qb->selectDistinct(['id', 'principaluri'])
|
||||
->from('calendarsubscriptions')
|
||||
->setMaxResults(self::SUBSCRIPTIONS_CHUNK_SIZE)
|
||||
->setFirstResult($this->progress);
|
||||
|
||||
$result = $query->execute();
|
||||
while ($row = $result->fetch()) {
|
||||
$username = $this->getPrincipal($row['principaluri']);
|
||||
if (!$this->userManager->userExists($username)) {
|
||||
$this->orphanSubscriptions[] = $row['id'];
|
||||
}
|
||||
}
|
||||
$result->closeCursor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function deleteOrphanSubscriptions(): void {
|
||||
foreach ($this->orphanSubscriptions as $orphanSubscriptionID) {
|
||||
$this->deleteOrphanSubscription($orphanSubscriptionID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function deleteOrphanSubscription(int $orphanSubscriptionID): void {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->delete('calendarsubscriptions')
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($orphanSubscriptionID)))
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
private function getPrincipal(string $principalUri): string {
|
||||
$uri = explode('/', $principalUri);
|
||||
return array_pop($uri);
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2021 Thomas Citharel <nextcloud@tcit.fr>
|
||||
*
|
||||
* @author Thomas Citharel <nextcloud@tcit.fr>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\DAV\Tests\unit\DAV\Migration;
|
||||
|
||||
use OCA\DAV\Migration\RemoveDeletedUsersCalendarSubscriptions;
|
||||
use OCP\DB\IResult;
|
||||
use OCP\DB\QueryBuilder\IExpressionBuilder;
|
||||
use OCP\DB\QueryBuilder\IFunctionBuilder;
|
||||
use OCP\DB\QueryBuilder\IParameter;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\DB\QueryBuilder\IQueryFunction;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Migration\IOutput;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class RemoveDeletedUsersCalendarSubscriptionsTest extends TestCase {
|
||||
/**
|
||||
* @var IDBConnection|MockObject
|
||||
*/
|
||||
private $dbConnection;
|
||||
/**
|
||||
* @var IUserManager|MockObject
|
||||
*/
|
||||
private $userManager;
|
||||
|
||||
/**
|
||||
* @var IOutput|MockObject
|
||||
*/
|
||||
private $output;
|
||||
/**
|
||||
* @var RemoveDeletedUsersCalendarSubscriptions
|
||||
*/
|
||||
private $migration;
|
||||
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->dbConnection = $this->createMock(IDBConnection::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->output = $this->createMock(IOutput::class);
|
||||
|
||||
$this->migration = new RemoveDeletedUsersCalendarSubscriptions($this->dbConnection, $this->userManager);
|
||||
}
|
||||
|
||||
public function testGetName(): void {
|
||||
$this->assertEquals(
|
||||
'Clean up old calendar subscriptions from deleted users that were not cleaned-up',
|
||||
$this->migration->getName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataTestRun
|
||||
* @param array $subscriptions
|
||||
* @param array $userExists
|
||||
* @param int $deletions
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testRun(array $subscriptions, array $userExists, int $deletions): void {
|
||||
$qb = $this->createMock(IQueryBuilder::class);
|
||||
|
||||
$qb->method('select')->willReturn($qb);
|
||||
|
||||
$functionBuilder = $this->createMock(IFunctionBuilder::class);
|
||||
|
||||
$qb->method('func')->willReturn($functionBuilder);
|
||||
$functionBuilder->method('count')->willReturn($this->createMock(IQueryFunction::class));
|
||||
|
||||
$qb->method('selectDistinct')
|
||||
->with(['id', 'principaluri'])
|
||||
->willReturn($qb);
|
||||
|
||||
$qb->method('from')
|
||||
->with('calendarsubscriptions')
|
||||
->willReturn($qb);
|
||||
|
||||
$qb->method('setMaxResults')
|
||||
->willReturn($qb);
|
||||
|
||||
$qb->method('setFirstResult')
|
||||
->willReturn($qb);
|
||||
|
||||
$result = $this->createMock(IResult::class);
|
||||
|
||||
$qb->method('execute')
|
||||
->willReturn($result);
|
||||
|
||||
$result->expects($this->at(0))
|
||||
->method('fetchOne')
|
||||
->willReturn(count($subscriptions));
|
||||
|
||||
$result
|
||||
->method('fetch')
|
||||
->willReturnOnConsecutiveCalls(...$subscriptions);
|
||||
|
||||
$qb->method('delete')
|
||||
->with('calendarsubscriptions')
|
||||
->willReturn($qb);
|
||||
|
||||
$expr = $this->createMock(IExpressionBuilder::class);
|
||||
|
||||
$qb->method('expr')->willReturn($expr);
|
||||
$qb->method('createNamedParameter')->willReturn($this->createMock(IParameter::class));
|
||||
$qb->method('where')->willReturn($qb);
|
||||
// Only when user exists
|
||||
$qb->expects($this->exactly($deletions))->method('executeStatement');
|
||||
|
||||
$this->dbConnection->method('getQueryBuilder')->willReturn($qb);
|
||||
|
||||
|
||||
$this->output->expects($this->once())->method('startProgress');
|
||||
|
||||
$this->output->expects($subscriptions === [] ? $this->never(): $this->once())->method('advance');
|
||||
if (count($subscriptions)) {
|
||||
$this->userManager->method('userExists')
|
||||
->willReturnCallback(function (string $username) use ($userExists) {
|
||||
return $userExists[$username];
|
||||
});
|
||||
}
|
||||
$this->output->expects($this->once())->method('finishProgress');
|
||||
$this->output->expects($this->once())->method('info')->with(sprintf('%d calendar subscriptions without an user have been cleaned up', $deletions));
|
||||
|
||||
$this->migration->run($this->output);
|
||||
}
|
||||
|
||||
public function dataTestRun(): array {
|
||||
return [
|
||||
[[], [], 0],
|
||||
[[[
|
||||
'id' => 1,
|
||||
'principaluri' => 'users/principals/foo1',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'principaluri' => 'users/principals/bar1',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'principaluri' => 'users/principals/bar1',
|
||||
]], ['foo1' => true, 'bar1' => false], 2]
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue