From c961634e182e17268bf2782dfa5f6df40df40fb4 Mon Sep 17 00:00:00 2001 From: SebastianKrupinski Date: Mon, 26 Oct 2026 12:14:18 -0400 Subject: [PATCH] fix(dav): detect truncated CardDAV sync results on all databases Signed-off-by: SebastianKrupinski --- apps/dav/lib/CardDAV/CardDavBackend.php | 4 ++- .../tests/unit/CardDAV/CardDavBackendTest.php | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 8002d82b71c65..a874bd5999c13 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -976,7 +976,8 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, // Fetching all changes $stmt = $qb->executeQuery(); - $rowCount = $stmt->rowCount(); + // rowCount() is not reliable for SELECT statements on all databases + $rowCount = 0; $changes = []; $highestSyncToken = 0; @@ -984,6 +985,7 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, // This loop ensures that any duplicates are overwritten, only the // last change on a node is relevant. while ($row = $stmt->fetchAssociative()) { + $rowCount++; $changes[$row['uri']] = $row['operation']; $highestSyncToken = $row['synctoken']; } diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php index 4d3e5b516efa2..bee0bcbf762d1 100644 --- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -607,6 +607,33 @@ public function testSyncSupport(): void { $this->assertEquals($uri0, $changes['added'][0]); } + public function testSyncSupportTruncatesIncrementalChanges(): void { + $this->config->method('getSystemValueInt') + ->willReturnCallback(fn (string $key, int $default) => $key === 'carddav_sync_request_truncation' ? 2 : $default); + + $bookId = $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); + $syncToken = $this->backend->getChangesForAddressBook($bookId, '', 1)['syncToken']; + + $uris = []; + for ($i = 0; $i < 3; $i++) { + $uri = $this->getUniqueID('card'); + $this->backend->createCard($bookId, $uri, str_replace('UID:Test', 'UID:Test' . $i, $this->vcardTest0)); + $uris[] = $uri; + } + + $changes = $this->backend->getChangesForAddressBook($bookId, (string)$syncToken, 1, null); + $this->assertTrue($changes['result_truncated']); + $this->assertSame([$uris[0], $uris[1]], $changes['added']); + + $changes = $this->backend->getChangesForAddressBook($bookId, (string)$changes['syncToken'], 1, null); + $this->assertTrue($changes['result_truncated']); + $this->assertSame([$uris[1], $uris[2]], $changes['added']); + + $changes = $this->backend->getChangesForAddressBook($bookId, (string)$changes['syncToken'], 1, null); + $this->assertArrayNotHasKey('result_truncated', $changes); + $this->assertSame([$uris[2]], $changes['added']); + } + public function testSharing(): void { $this->userManager->expects($this->any()) ->method('userExists')