diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index 2ec645f04d231..bcfd68943ad3c 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 @@ private function canWrite(): bool { } 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 b15ed60707685..404b3042bf1e9 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -23,9 +23,11 @@ use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; use PDO; +use Psr\Log\LoggerInterface; use Sabre\CardDAV\Backend\BackendInterface; use Sabre\CardDAV\Backend\SyncSupport; use Sabre\CardDAV\Plugin; @@ -59,6 +61,8 @@ public function __construct( private IUserManager $userManager, private IEventDispatcher $dispatcher, private Sharing\Backend $sharingBackend, + private LoggerInterface $logger, + private IConfig $config, ) { } @@ -851,6 +855,10 @@ public function deleteCard($addressBookId, $cardUri) { * @return array */ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { + if ($limit === null) { + $limit = $this->config->getSystemValueInt('carddav_sync_request_limit', 1); + + } // Current synctoken return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) { $qb = $this->db->getQueryBuilder(); @@ -873,8 +881,34 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, 'modified' => [], 'deleted' => [], ]; - - if ($syncToken) { + $this->logger->error('getChangesForAddressBook', ['syncToken' => $syncToken, 'currentToken' => $currentToken, 'limit' => $limit]); + if (str_starts_with($syncToken, 'init_')) { + $syncValues = explode('_', $syncToken); + $lastID = $syncValues[1]; + $initialSyncToken = $syncValues[2]; + $qb = $this->db->getQueryBuilder(); + $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 = end($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') ->from('addressbookchanges') @@ -899,6 +933,8 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, // 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(); @@ -917,14 +953,28 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, } } 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. - $stmt = $qb->executeQuery(); - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + if (is_int($limit) && $limit > 0) { + $qb->setMaxResults($limit); + $stmt = $qb->executeQuery(); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $lastID = end($values)['id']; + if (count(array_values($values)) === $limit) { + $result['syncToken'] = 'init_' . $lastID . '_' . $currentToken; + $result['result_truncated'] = true; + } + } else { + $stmt = $qb->executeQuery(); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $this->logger->error('getChangesForAddressBook', ['values' => $values]); + } + $result['added'] = array_column($values, 'uri'); + $stmt->closeCursor(); } return $result; diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index cc3d324faf1ff..1c99a5ef356bd 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -63,6 +63,7 @@ public function syncRemoteAddressBook(string $url, string $userName, string $add $this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]); throw $ex; } + $this->logger->error('sync response', [$response]); // 3. apply changes // TODO: use multi-get for download diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index e0032044e701d..912a2f1dcee05 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -8,7 +8,6 @@ */ namespace OCA\DAV\CardDAV; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCA\Federation\TrustedServers; use OCP\Accounts\IAccountManager; use OCP\IConfig; @@ -212,14 +211,7 @@ public function getChild($name): Card { } 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 751ab17bb7aa6..014c9b192a519 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -124,6 +124,8 @@ public function __construct() { ); $contactsSharingBackend = \OC::$server->get(\OCA\DAV\CardDAV\Sharing\Backend::class); + $logger = \OC::$server->get(\Psr\Log\LoggerInterface::class); + $config = \OC::$server->get(IConfig::class); $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); $usersCardDavBackend = new CardDavBackend( @@ -132,6 +134,8 @@ public function __construct() { $userManager, $dispatcher, $contactsSharingBackend, + $logger, + $config ); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; @@ -142,6 +146,8 @@ public function __construct() { $userManager, $dispatcher, $contactsSharingBackend, + $logger, + $config ); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index f07927ff0f95b..2ed316d7ee240 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -160,7 +160,8 @@ public function __construct( $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger)); $this->server->addPlugin(new LockPlugin()); - $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin()); + $logger = \OC::$server->get(LoggerInterface::class); + $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin($logger)); // acl $acl = new DavAclPlugin(); diff --git a/apps/federation/lib/SyncFederationAddressBooks.php b/apps/federation/lib/SyncFederationAddressBooks.php index 05144b40879bf..53931dd1675f9 100644 --- a/apps/federation/lib/SyncFederationAddressBooks.php +++ b/apps/federation/lib/SyncFederationAddressBooks.php @@ -52,6 +52,10 @@ public function syncThemAll(\Closure $callback) { 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) { + $newToken = $this->syncTruncatedAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $newToken, $targetBookId, $targetPrincipal, $targetBookProperties); + } $this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK, $newToken); } else { $this->logger->debug("Sync Token for $url unchanged from previous sync"); @@ -76,4 +80,12 @@ public function syncThemAll(\Closure $callback) { } } } + + private function syncTruncatedAddressBook(string $url, string $cardDavUser, string $addressBookUrl, string $sharedSecret, string $syncToken, int $targetBookId, string $targetPrincipal, array $targetBookProperties): string { + $newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties); + while (strpos($newToken, 'init') !== false) { + $newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties); + } + return $newToken; + } } diff --git a/config/config.sample.php b/config/config.sample.php index bfda1609d751a..f8cb906a7ada1 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -346,6 +346,11 @@ */ 'carddav_sync_request_timeout' => 30, +/** + * The limit applied to the initial synchronization report request, e.g. federated system address books (as run by `occ federation:sync-addressbooks`). + */ +'carddav_initial_sync_request_limit' => 1000, + /** * `true` enabled a relaxed session timeout, where the session timeout would no longer be * handled by Nextcloud but by either the PHP garbage collection or the expiration of diff --git a/lib/public/Http/Client/IClient.php b/lib/public/Http/Client/IClient.php index e4f46d44e4dfa..800a7aaa69e33 100644 --- a/lib/public/Http/Client/IClient.php +++ b/lib/public/Http/Client/IClient.php @@ -22,6 +22,14 @@ interface IClient { */ public const DEFAULT_REQUEST_TIMEOUT = 30; + /** + * Default limit for address book intial sync + * + * @since 31.0.0 + */ + + public const DEFAULT_ADDRESSBOOK_INITIAL_SYNC_LIMIT = 1; + /** * Sends a GET request * @param string $uri