Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions apps/dav/lib/CardDAV/CardDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,8 @@ public function updateCard($addressBookId, $cardUri, $cardData) {
return '"' . $etag . '"';
}

$storedEtag = $this->getCardEtag($addressBookId, $cardUri);

$query->update($this->dbCardsTable)
->set('carddata', $query->createNamedParameter($cardData, IQueryBuilder::PARAM_LOB))
->set('lastmodified', $query->createNamedParameter(time()))
Expand All @@ -735,9 +737,14 @@ public function updateCard($addressBookId, $cardUri, $cardData) {

$this->etagCache[$etagCacheKey] = $etag;

$this->addChange($addressBookId, $cardUri, 2);
$this->updateProperties($addressBookId, $cardUri, $cardData);

if ($storedEtag === $etag) {
return '"' . $etag . '"';
}

$this->addChange($addressBookId, $cardUri, 2);

$addressBookData = $this->getAddressBookById($addressBookId);
$shares = $this->getShares($addressBookId);
$objectRow = $this->getCard($addressBookId, $cardUri);
Expand Down Expand Up @@ -820,9 +827,9 @@ public function deleteCard($addressBookId, $cardUri) {
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($cardUri)))
->executeStatement();

$this->addChange($addressBookId, $cardUri, 3);

if ($ret === 1) {
$this->addChange($addressBookId, $cardUri, 3);

if ($cardId !== null) {
$this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, $addressBookData, $shares, $objectRow));
$this->purgeProperties($addressBookId, $cardId);
Expand Down Expand Up @@ -1486,6 +1493,22 @@ protected function getCardId(int $addressBookId, string $uri): int {
return (int)$cardIds['id'];
}

/**
* Get the etag currently stored for a contact, or null if there is none
*/
protected function getCardEtag(int $addressBookId, string $uri): ?string {
$query = $this->db->getQueryBuilder();
$query->select('etag')->from($this->dbCardsTable)
->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
->andWhere($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));

$result = $query->executeQuery();
$etag = $result->fetchOne();
$result->closeCursor();

return $etag === false ? null : (string)$etag;
}

/**
* For shared address books the sharee is set in the ACL of the address book
*
Expand Down
109 changes: 102 additions & 7 deletions apps/dav/tests/unit/CardDAV/CardDavBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,7 @@ protected function setUp(): void {
$this->createMock(LoggerInterface::class)
);

$this->backend = new CardDavBackend($this->db,
$this->principal,
$this->userManager,
$this->dispatcher,
$this->sharingBackend,
$this->config,
);
$this->backend = $this->createBackend();
// start every test with a empty cards_properties and cards table
$query = $this->db->getQueryBuilder();
$query->delete('cards_properties')->executeStatement();
Expand All @@ -163,6 +157,44 @@ protected function setUp(): void {
}
}

/**
* A fresh instance starts with an empty in-memory etag cache, like a
* subsequent request or background job would.
*/
private function createBackend(): CardDavBackend {
return new CardDavBackend($this->db,
$this->principal,
$this->userManager,
$this->dispatcher,
$this->sharingBackend,
$this->config,
);
}

private function countChanges(int $addressBookId): int {
$query = $this->db->getQueryBuilder();
$query->select($query->func()->count('*'))
->from('addressbookchanges')
->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId)));
$result = $query->executeQuery();
$count = (int)$result->fetchOne();
$result->closeCursor();

return $count;
}

private function getSyncToken(int $addressBookId): int {
$query = $this->db->getQueryBuilder();
$query->select('synctoken')
->from('addressbooks')
->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId)));
$result = $query->executeQuery();
$syncToken = (int)$result->fetchOne();
$result->closeCursor();

return $syncToken;
}

protected function tearDown(): void {
if (is_null($this->backend)) {
return;
Expand Down Expand Up @@ -486,6 +518,69 @@ public function testDeleteWithoutCard(): void {
$this->assertTrue($this->backend->deleteCard($bookId, $uri));
}

public function testDeleteCardWithoutMatchingRowRecordsNoChange(): void {
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER);
$bookId = (int)$books[0]['id'];

$this->assertFalse($this->backend->deleteCard($bookId, 'does-not-exist.vcf'));

$this->assertSame(0, $this->countChanges($bookId));
$this->assertSame(1, $this->getSyncToken($bookId));
}

public function testUpdateCardWithUnchangedDataRecordsNoChange(): void {
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER);
$bookId = (int)$books[0]['id'];

$uri = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri, $this->vcardTest0);
$changesAfterCreate = $this->countChanges($bookId);
$syncTokenAfterCreate = $this->getSyncToken($bookId);

$etag = $this->createBackend()->updateCard($bookId, $uri, $this->vcardTest0);

$this->assertEquals('"' . md5($this->vcardTest0) . '"', $etag);
$this->assertSame($changesAfterCreate, $this->countChanges($bookId));
$this->assertSame($syncTokenAfterCreate, $this->getSyncToken($bookId));
}

public function testUpdateCardWithChangedDataRecordsChange(): void {
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER);
$bookId = (int)$books[0]['id'];

$uri = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri, $this->vcardTest0);
$changesAfterCreate = $this->countChanges($bookId);
$syncTokenAfterCreate = $this->getSyncToken($bookId);

$this->createBackend()->updateCard($bookId, $uri, $this->vcardTest1);

$this->assertSame($changesAfterCreate + 1, $this->countChanges($bookId));
$this->assertSame($syncTokenAfterCreate + 1, $this->getSyncToken($bookId));
}

/**
* Federated full syncs null `lastmodified` up front and delete whatever is
* still null afterwards, so an unchanged card must still refresh the column.
*/
public function testUpdateCardWithUnchangedDataClearsPendingState(): void {
$this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []);
$books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER);
$bookId = (int)$books[0]['id'];

$uri = $this->getUniqueID('card');
$this->backend->createCard($bookId, $uri, $this->vcardTest0);
$this->backend->markCardsAsPending($bookId);
$this->assertCount(1, $this->backend->getPendingCards($bookId));

$this->createBackend()->updateCard($bookId, $uri, $this->vcardTest0);

$this->assertEmpty($this->backend->getPendingCards($bookId));
}

public function testSyncSupport(): void {
$this->backend = $this->getMockBuilder(CardDavBackend::class)
->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config])
Expand Down
Loading