2222use Sabre \DAV \Xml \Response \MultiStatus ;
2323use Sabre \DAV \Xml \Service ;
2424use Sabre \VObject \Reader ;
25+ use Sabre \Xml \ParseException ;
2526use function is_null ;
2627
2728class 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 /**
0 commit comments