From 84c0e1be9a965f2b750ce4ff26b1b76df9e5c64c Mon Sep 17 00:00:00 2001 From: Hamza Mahjoubi Date: Wed, 8 Jan 2025 20:39:18 +0700 Subject: [PATCH] feat(cardav): support result truncation for addressbook federation Signed-off-by: Hamza Mahjoubi --- apps/dav/lib/CardDAV/AddressBook.php | 4 -- apps/dav/lib/CardDAV/CardDavBackend.php | 61 +++++++++++++++++-- apps/dav/lib/CardDAV/SystemAddressbook.php | 8 --- apps/dav/lib/RootCollection.php | 2 + .../tests/unit/CardDAV/CardDavBackendTest.php | 8 ++- .../tests/unit/CardDAV/SyncServiceTest.php | 12 ++-- .../lib/SyncFederationAddressBooks.php | 6 ++ 7 files changed, 74 insertions(+), 27 deletions(-) diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index 2ec645f04d2..bcfd68943ad 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -8,7 +8,6 @@ namespace OCA\DAV\CardDAV; use OCA\DAV\DAV\Sharing\IShareable; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCP\DB\Exception; use OCP\IL10N; use OCP\Server; @@ -234,9 +233,6 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov } public function getChanges($syncToken, $syncLevel, $limit = null) { - if (!$syncToken && $limit) { - throw new UnsupportedLimitOnInitialSyncException(); - } return parent::getChanges($syncToken, $syncLevel, $limit); } diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index fcd318cd406..eaaa569be1b 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -23,6 +23,7 @@ use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; use PDO; @@ -59,6 +60,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { private IUserManager $userManager, private IEventDispatcher $dispatcher, private Sharing\Backend $sharingBackend, + private IConfig $config, ) { } @@ -850,6 +852,8 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return array */ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { + $maxLimit = $this->config->getSystemValueInt('carddav_sync_request_truncation', 2500); + $limit = ($limit === null) ? $maxLimit : min($limit, $maxLimit); // Current synctoken return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) { $qb = $this->db->getQueryBuilder(); @@ -872,10 +876,35 @@ class CardDavBackend implements BackendInterface, SyncSupport { 'modified' => [], 'deleted' => [], ]; - - if ($syncToken) { + if (str_starts_with($syncToken, 'init_')) { + $syncValues = explode('_', $syncToken); + $lastID = $syncValues[1]; + $initialSyncToken = $syncValues[2]; $qb = $this->db->getQueryBuilder(); - $qb->select('uri', 'operation') + $qb->select('id', 'uri') + ->from('cards') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)), + $qb->expr()->gt('id', $qb->createNamedParameter($lastID))) + )->orderBy('id') + ->setMaxResults($limit); + $stmt = $qb->executeQuery(); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if (count($values) === 0) { + $result['syncToken'] = $initialSyncToken; + $result['result_truncated'] = false; + $result['added'] = []; + } else { + $lastID = $values[array_key_last($values)]['id']; + $result['added'] = array_column($values, 'uri'); + $result['syncToken'] = count($result['added']) >= $limit ? "init_{$lastID}_$initialSyncToken" : $initialSyncToken ; + $result['result_truncated'] = count($result['added']) >= $limit; + } + } elseif ($syncToken) { + $qb = $this->db->getQueryBuilder(); + $qb->select('uri', 'operation', 'synctoken') ->from('addressbookchanges') ->where( $qb->expr()->andX( @@ -885,7 +914,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { ) )->orderBy('synctoken'); - if (is_int($limit) && $limit > 0) { + if ($limit > 0) { $qb->setMaxResults($limit); } @@ -898,8 +927,15 @@ class CardDavBackend implements BackendInterface, SyncSupport { // last change on a node is relevant. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $changes[$row['uri']] = $row['operation']; + // get the last synctoken, needed in case a limit was set + $result['syncToken'] = $row['synctoken']; } $stmt->closeCursor(); + + // No changes found, use current token + if (empty($changes)) { + $result['syncToken'] = $currentToken; + } foreach ($changes as $uri => $operation) { switch ($operation) { @@ -916,14 +952,27 @@ class CardDavBackend implements BackendInterface, SyncSupport { } } else { $qb = $this->db->getQueryBuilder(); - $qb->select('uri') + $qb->select('id', 'uri') ->from('cards') ->where( $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) ); // No synctoken supplied, this is the initial sync. + $qb->setMaxResults($limit); $stmt = $qb->executeQuery(); - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + if (empty($values)) { + $result['added'] = []; + return $result; + } + $lastID = $values[array_key_last($values)]['id']; + if (count($values) >= $limit) { + $result['syncToken'] = 'init_' . $lastID . '_' . $currentToken; + $result['result_truncated'] = true; + } + + $result['added'] = array_column($values, 'uri'); + $stmt->closeCursor(); } return $result; diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index e0032044e70..912a2f1dcee 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -8,7 +8,6 @@ declare(strict_types=1); */ namespace OCA\DAV\CardDAV; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCA\Federation\TrustedServers; use OCP\Accounts\IAccountManager; use OCP\IConfig; @@ -212,14 +211,7 @@ class SystemAddressbook extends AddressBook { } return new Card($this->carddavBackend, $this->addressBookInfo, $obj); } - - /** - * @throws UnsupportedLimitOnInitialSyncException - */ public function getChanges($syncToken, $syncLevel, $limit = null) { - if (!$syncToken && $limit) { - throw new UnsupportedLimitOnInitialSyncException(); - } if (!$this->carddavBackend instanceof SyncSupport) { return null; diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 751ab17bb7a..d35cf502c24 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -132,6 +132,7 @@ class RootCollection extends SimpleCollection { $userManager, $dispatcher, $contactsSharingBackend, + $config ); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; @@ -142,6 +143,7 @@ class RootCollection extends SimpleCollection { $userManager, $dispatcher, $contactsSharingBackend, + $config ); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php index 96968909d1c..dcb12d15bb7 100644 --- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -108,6 +108,7 @@ class CardDavBackendTest extends TestCase { $this->userManager = $this->createMock(IUserManager::class); $this->groupManager = $this->createMock(IGroupManager::class); + $this->config = $this->createMock(IConfig::class); $this->principal = $this->getMockBuilder(Principal::class) ->setConstructorArgs([ $this->userManager, @@ -118,7 +119,7 @@ class CardDavBackendTest extends TestCase { $this->createMock(IAppManager::class), $this->createMock(ProxyMapper::class), $this->createMock(KnownUserService::class), - $this->createMock(IConfig::class), + $this->config, $this->createMock(IFactory::class) ]) ->setMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) @@ -147,6 +148,7 @@ class CardDavBackendTest extends TestCase { $this->userManager, $this->dispatcher, $this->sharingBackend, + $this->config, ); // start every test with a empty cards_properties and cards table $query = $this->db->getQueryBuilder(); @@ -405,7 +407,7 @@ class CardDavBackendTest extends TestCase { public function testDeleteWithoutCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods([ 'getCardId', 'addChange', @@ -513,7 +515,7 @@ class CardDavBackendTest extends TestCase { $cardId = 2; $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods(['getCardId'])->getMock(); $backend->expects($this->any())->method('getCardId')->willReturn($cardId); diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index 5af42e2ea4e..fc27a654c00 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -104,7 +104,7 @@ class SyncServiceTest extends TestCase { '1', 'principals/system/system', [] - ); + )['token']; $this->assertEquals('http://sabre.io/ns/sync/1', $token); } @@ -175,7 +175,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )['token']; $this->assertEquals('http://sabre.io/ns/sync/2', $token); } @@ -246,7 +246,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )['token']; $this->assertEquals('http://sabre.io/ns/sync/3', $token); } @@ -287,7 +287,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )['token']; $this->assertEquals('http://sabre.io/ns/sync/4', $token); } @@ -430,7 +430,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )['token']; } /** @@ -472,7 +472,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )['token']; } public function providerUseAbsoluteUriReport(): array { diff --git a/apps/federation/lib/SyncFederationAddressBooks.php b/apps/federation/lib/SyncFederationAddressBooks.php index 05144b40879..7f700cde21b 100644 --- a/apps/federation/lib/SyncFederationAddressBooks.php +++ b/apps/federation/lib/SyncFederationAddressBooks.php @@ -52,6 +52,12 @@ class SyncFederationAddressBooks { try { $newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties); if ($newToken !== $syncToken) { + // Finish truncated initial sync. + if (strpos($newToken, 'init') !== false) { + do { + $newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties); + } while (str_contains($newToken, 'init_')); + } $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK, $newToken); } else { $this->logger->debug("Sync Token for $url unchanged from previous sync");