Skip to content

Commit bf8dde7

Browse files
committed
feat(carddav): handle truncated non-initial requests
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
1 parent 5ca43d6 commit bf8dde7

File tree

2 files changed

+67
-23
lines changed

2 files changed

+67
-23
lines changed

apps/dav/lib/CardDAV/SyncService.php

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Sabre\DAV\Xml\Response\MultiStatus;
2323
use Sabre\DAV\Xml\Service;
2424
use Sabre\VObject\Reader;
25+
use Sabre\Xml\ParseException;
2526
use function is_null;
2627

2728
class SyncService {
@@ -43,9 +44,10 @@ public function __construct(
4344
}
4445

4546
/**
47+
* @psalm-return list{0: ?string, 1: boolean}
4648
* @throws \Exception
4749
*/
48-
public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string {
50+
public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): array {
4951
// 1. create addressbook
5052
$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
5153
$addressBookId = $book['id'];
@@ -83,7 +85,10 @@ public function syncRemoteAddressBook(string $url, string $userName, string $add
8385
}
8486
}
8587

86-
return $response['token'];
88+
return [
89+
$response['token'],
90+
$response['truncated'],
91+
];
8792
}
8893

8994
/**
@@ -127,7 +132,7 @@ public function ensureLocalSystemAddressBookExists(): ?array {
127132

128133
private function prepareUri(string $host, string $path): string {
129134
/*
130-
* The trailing slash is important for merging the uris together.
135+
* The trailing slash is important for merging the uris.
131136
*
132137
* $host is stored in oc_trusted_servers.url and usually without a trailing slash.
133138
*
@@ -158,7 +163,9 @@ private function prepareUri(string $host, string $path): string {
158163
}
159164

160165
/**
166+
* @return array{response: array{string, array<array-key, mixed}, token: ?string, truncated: boolean}
161167
* @throws ClientExceptionInterface
168+
* @throws ParseException
162169
*/
163170
protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
164171
$client = $this->clientService->newClient();
@@ -180,7 +187,7 @@ protected function requestSyncReport(string $url, string $userName, string $addr
180187
$body = $response->getBody();
181188
assert(is_string($body));
182189

183-
return $this->parseMultiStatus($body);
190+
return $this->parseMultiStatus($body, $addressBookUrl);
184191
}
185192

186193
protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string {
@@ -217,22 +224,50 @@ private function buildSyncCollectionRequestBody(?string $syncToken): string {
217224
}
218225

219226
/**
220-
* @param string $body
221-
* @return array
222-
* @throws \Sabre\Xml\ParseException
227+
* @return array{response: array{string, array<array-key, mixed}, token: ?string, truncated: boolean}
228+
* @throws ParseException
223229
*/
224-
private function parseMultiStatus($body) {
225-
$xml = new Service();
226-
230+
private function parseMultiStatus(string $body, string $addressBookUrl): array {
227231
/** @var MultiStatus $multiStatus */
228-
$multiStatus = $xml->expect('{DAV:}multistatus', $body);
232+
$multiStatus = (new Service())->expect('{DAV:}multistatus', $body);
229233

230234
$result = [];
235+
$truncated = false;
236+
231237
foreach ($multiStatus->getResponses() as $response) {
232-
$result[$response->getHref()] = $response->getResponseProperties();
238+
$href = $response->getHref();
239+
if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $addressBookUrl)) {
240+
$truncated = true;
241+
} else {
242+
$result[$response->getHref()] = $response->getResponseProperties();
243+
}
233244
}
234245

235-
return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
246+
return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated];
247+
}
248+
249+
/**
250+
* Determines whether the provided response URI corresponds to the given request URI.
251+
*/
252+
private function isResponseForRequestUri(string $responseUri, string $requestUri): bool {
253+
/*
254+
* Example response uri:
255+
*
256+
* /remote.php/dav/addressbooks/system/system/system/
257+
* /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory)
258+
*
259+
* Example request uri:
260+
*
261+
* remote.php/dav/addressbooks/system/system/system
262+
*
263+
* References:
264+
* https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174
265+
* https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41
266+
*/
267+
return str_ends_with(
268+
rtrim($responseUri, '/'),
269+
rtrim($requestUri, '/')
270+
);
236271
}
237272

238273
/**

apps/federation/lib/SyncFederationAddressBooks.php

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function syncThemAll(\Closure $callback) {
3434
$url = $trustedServer['url'];
3535
$callback($url, null);
3636
$sharedSecret = $trustedServer['shared_secret'];
37-
$syncToken = $trustedServer['sync_token'];
37+
$oldSyncToken = $trustedServer['sync_token'];
3838

3939
$endPoints = $this->ocsDiscoveryService->discover($url, 'FEDERATED_SHARING');
4040
$cardDavUser = $endPoints['carddav-user'] ?? 'system';
@@ -49,16 +49,25 @@ public function syncThemAll(\Closure $callback) {
4949
$targetBookProperties = [
5050
'{DAV:}displayname' => $url
5151
];
52+
5253
try {
53-
$newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties);
54-
if ($newToken !== $syncToken) {
55-
// Finish truncated initial sync.
56-
if (strpos($newToken, 'init') !== false) {
57-
do {
58-
$newToken = $this->syncService->syncRemoteAddressBook($url, $cardDavUser, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetBookProperties);
59-
} while (str_contains($newToken, 'init_'));
60-
}
61-
$this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK, $newToken);
54+
$syncToken = $oldSyncToken;
55+
56+
do {
57+
[$syncToken, $truncated] = $this->syncService->syncRemoteAddressBook(
58+
$url,
59+
$cardDavUser,
60+
$addressBookUrl,
61+
$sharedSecret,
62+
$syncToken,
63+
$targetBookId,
64+
$targetPrincipal,
65+
$targetBookProperties
66+
);
67+
} while ($truncated);
68+
69+
if ($syncToken !== $oldSyncToken) {
70+
$this->dbHandler->setServerStatus($url, TrustedServers::STATUS_OK, $syncToken);
6271
} else {
6372
$this->logger->debug("Sync Token for $url unchanged from previous sync");
6473
// The server status might have been changed to a failure status in previous runs.

0 commit comments

Comments
 (0)