diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index 1b5e85fb83c83..0bc5c57a40636 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -66,11 +66,13 @@ public function syncRemoteAddressBook(string $url, string $userName, string $add throw $ex; } + $received = []; // 3. apply changes // TODO: use multi-get for download foreach ($response['response'] as $resource => $status) { $cardUri = basename($resource); if (isset($status[200])) { + $received[] = $cardUri; $absoluteUrl = $this->prepareUri($url, $resource); $vCard = $this->download($absoluteUrl, $userName, $sharedSecret); $this->atomic(function () use ($addressBookId, $cardUri, $vCard): void { @@ -86,6 +88,15 @@ public function syncRemoteAddressBook(string $url, string $userName, string $add } } + // when doing a full sync, remove any items in the local address book that aren't in the remote one + if (!$syncToken) { + $existingCards = $this->backend->getCards($addressBookId); + $removedCards = array_filter($existingCards, fn (array $card) => !in_array($card['uri'], $received)); + foreach ($removedCards as $removedCard) { + $this->backend->deleteCard($addressBookId, $removedCard['uri']); + } + } + return [ $response['token'], $response['truncated'], diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index 912a2f1dcee05..636089dbab084 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -232,7 +232,8 @@ public function getChanges($syncToken, $syncLevel, $limit = null) { return $changed; } - $added = $modified = $deleted = []; + $added = $modified = []; + $deleted = array_values($changed['deleted']); foreach ($changed['added'] as $uri) { try { $this->getChild($uri); diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index 9411bb20e4ae2..95474ff6b6921 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -104,7 +104,7 @@ public function testEmptySync(): void { 'system', 'system', '1234567890', - null, + '1', '1', 'principals/system/system', [] @@ -175,7 +175,7 @@ public function testSyncWithNewElement(): void { 'system', 'system', '1234567890', - null, + '1', '1', 'principals/system/system', [] @@ -246,7 +246,7 @@ public function testSyncWithUpdatedElement(): void { 'system', 'system', '1234567890', - null, + '1', '1', 'principals/system/system', [] @@ -287,7 +287,7 @@ public function testSyncWithDeletedElement(): void { 'system', 'system', '1234567890', - null, + '1', '1', 'principals/system/system', [] @@ -296,6 +296,79 @@ public function testSyncWithDeletedElement(): void { $this->assertEquals('http://sabre.io/ns/sync/4', $token); } + public function testFullSyncWithOrphanElement(): void { + $this->backend->expects($this->exactly(0)) + ->method('createCard'); + $this->backend->expects($this->exactly(1)) + ->method('updateCard'); + $this->backend->expects($this->exactly(1)) + ->method('deleteCard'); + + $body = ' + + + /remote.php/dav/addressbooks/system/system/system/Database:alice.vcf + + + text/vcard; charset=utf-8 + "2df155fa5c2a24cd7f750353fc63f037" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/3 +'; + + $reportResponse = new Response(new PsrResponse( + 207, + ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)], + $body + )); + + $this->client + ->method('request') + ->willReturn($reportResponse); + + $vCard = 'BEGIN:VCARD +VERSION:3.0 +PRODID:-//Sabre//Sabre VObject 4.5.4//EN +UID:alice +FN;X-NC-SCOPE=v2-federated:alice +N;X-NC-SCOPE=v2-federated:alice;;;; +X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice +CLOUD:alice@server2.internal +END:VCARD'; + + $getResponse = new Response(new PsrResponse( + 200, + ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)], + $vCard, + )); + + $this->client + ->method('get') + ->willReturn($getResponse); + + $this->backend->method('getCards') + ->willReturn([ + ['uri' => 'Database:alice.vcf'], + ['uri' => 'Database:bob.vcf'], + ]); + + $token = $this->service->syncRemoteAddressBook( + '', + 'system', + 'system', + '1234567890', + null, + '1', + 'principals/system/system', + [] + )[0]; + + $this->assertEquals('http://sabre.io/ns/sync/3', $token); + } + public function testEnsureSystemAddressBookExists(): void { /** @var CardDavBackend&MockObject $backend */ $backend = $this->createMock(CardDavBackend::class); @@ -458,7 +531,7 @@ public function testUseAbsoluteUriReport(string $host, string $expected): void { 'system', 'remote.php/dav/addressbooks/system/system/system', '1234567890', - null, + '1', '1', 'principals/system/system', [] diff --git a/apps/federation/lib/Command/SyncFederationAddressBooks.php b/apps/federation/lib/Command/SyncFederationAddressBooks.php index 0325d875969dc..72d6a9099eea3 100644 --- a/apps/federation/lib/Command/SyncFederationAddressBooks.php +++ b/apps/federation/lib/Command/SyncFederationAddressBooks.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class SyncFederationAddressBooks extends Command { @@ -25,19 +26,21 @@ public function __construct( protected function configure() { $this ->setName('federation:sync-addressbooks') - ->setDescription('Synchronizes addressbooks of all federated clouds'); + ->setDescription('Synchronizes addressbooks of all federated clouds') + ->addOption('full', null, InputOption::VALUE_NONE, 'Perform a full sync instead of a delta sync'); } protected function execute(InputInterface $input, OutputInterface $output): int { $progress = new ProgressBar($output); $progress->start(); + $full = (bool)$input->getOption('full'); $this->syncService->syncThemAll(function ($url, $ex) use ($progress, $output): void { if ($ex instanceof \Exception) { $output->writeln("Error while syncing $url : " . $ex->getMessage()); } else { $progress->advance(); } - }); + }, $full); $progress->finish(); $output->writeln(''); diff --git a/apps/federation/lib/SyncFederationAddressBooks.php b/apps/federation/lib/SyncFederationAddressBooks.php index 51af00310e8c6..ac51627a81dfe 100644 --- a/apps/federation/lib/SyncFederationAddressBooks.php +++ b/apps/federation/lib/SyncFederationAddressBooks.php @@ -24,7 +24,7 @@ public function __construct( /** * @param \Closure $callback */ - public function syncThemAll(\Closure $callback) { + public function syncThemAll(\Closure $callback, bool $full = false) { $trustedServers = $this->dbHandler->getAllServer(); foreach ($trustedServers as $trustedServer) { $url = $trustedServer['url']; @@ -55,7 +55,7 @@ public function syncThemAll(\Closure $callback) { $cardDavUser, $addressBookUrl, $sharedSecret, - $syncToken, + $full ? null : $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties