From 2e127ad9ebeb929d280d5d188a65e08b08085a48 Mon Sep 17 00:00:00 2001 From: Hamza Date: Fri, 28 Aug 2026 13:03:30 +0200 Subject: [PATCH] fix(cardDav): Only update user card on actual mapped propreties changes fix(cardDav): Only update user card on actual mapped propreties changes Signed-off-by: Hamza --- apps/dav/lib/CardDAV/SyncService.php | 7 +++- .../tests/unit/CardDAV/SyncServiceTest.php | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index 38c2cef83a58d..793c43ecd0854 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -152,7 +152,12 @@ public function updateUser(IUser $user): void { if (is_null($vCard)) { $this->backend->deleteCard($addressBookId, $cardId); } else { - $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize()); + $cardData = $vCard->serialize(); + // Writing an identical card would still bump the address book + // sync token and make every client re-download the card + if ($card['carddata'] !== $cardData) { + $this->backend->updateCard($addressBookId, $cardId, $cardData); + } } } }, $this->dbConnection); diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index e9efe4972ef93..4e4fa0d3ed679 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -459,6 +459,45 @@ public function testUpdateAndDeleteUser(bool $activated, int $createCalls, int $ $ss->deleteUser($user); } + public function testUpdateUserSkipsUnchangedCard(): void { + $vCard = new VCard(); + $vCard->VERSION = '3.0'; + $vCard->UID = 'test-user'; + $vCard->FN = 'test-user'; + + /** @var CardDavBackend&MockObject $backend */ + $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock(); + $logger = $this->createMock(LoggerInterface::class); + + $backend->expects($this->never())->method('createCard'); + $backend->expects($this->never())->method('updateCard'); + $backend->expects($this->never())->method('deleteCard'); + + $backend->method('getCard')->willReturn(['carddata' => $vCard->serialize()]); + $backend->method('getAddressBooksByUri') + ->with('principals/system/system', 'system') + ->willReturn(['id' => -1]); + + $user = $this->createMock(IUser::class); + $user->method('getBackendClassName')->willReturn('unittest'); + $user->method('getUID')->willReturn('test-user'); + $user->method('isEnabled')->willReturn(true); + + $converter = $this->createMock(Converter::class); + $converter->method('createCardFromUser')->willReturn($vCard); + + $ss = new SyncService( + $this->createMock(IClientService::class), + $this->createMock(IConfig::class), + $backend, + $this->createMock(IUserManager::class), + $this->createMock(IDBConnection::class), + $logger, + $converter, + ); + $ss->updateUser($user); + } + public function testDeleteAddressbookWhenAccessRevoked(): void { $this->expectException(ClientExceptionInterface::class);