Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public function syncOne(FederatedCalendarEntity $calendar): int {
if (isset($status[200])) {
// Object created or updated
$absoluteUrl = $this->prepareUri($credentials['remoteUrl'], $resource);
$calendarData = $this->download($absoluteUrl, $credentials['username'], $credentials['token']);
$calendarData = $this->requestGet($absoluteUrl, $credentials['username'], $credentials['token']);
$existingObject = $this->backend->getCalendarObject(
$calendar->getId(),
$objectUri,
Expand Down
2 changes: 1 addition & 1 deletion apps/dav/lib/CalDAV/SyncService.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function syncRemoteCalendar(
$objectUri = basename($resource);
if (isset($status[200])) {
$absoluteUrl = $this->prepareUri($url, $resource);
$vCard = $this->download($absoluteUrl, $username, $sharedSecret);
$vCard = $this->requestGet($absoluteUrl, $username, $sharedSecret);
$existingObject = $this->backend->getCalendarObject($calendar->getId(), $objectUri, CalDavBackend::CALENDAR_TYPE_FEDERATED);
if (!$existingObject) {
$this->backend->createCalendarObject($calendar->getId(), $objectUri, $vCard, CalDavBackend::CALENDAR_TYPE_FEDERATED);
Expand Down
43 changes: 37 additions & 6 deletions apps/dav/lib/CardDAV/SyncService.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,24 @@ public function syncRemoteAddressBook(string $url, string $userName, string $add
}

// 3. apply changes
// TODO: use multi-get for download
$changedResources = [];
foreach ($response['response'] as $resource => $status) {
$cardUri = basename($resource);
if (isset($status[200])) {
$absoluteUrl = $this->prepareUri($url, $resource);
$vCard = $this->download($absoluteUrl, $userName, $sharedSecret);
$changedResources[] = $resource;
} else {
$this->backend->deleteCard($addressBookId, basename($resource));
}
}

if ($changedResources !== []) {
foreach ($this->fetchCards($url, $absoluteUri, $userName, $sharedSecret, $changedResources) as $resource => $vCard) {
$cardUri = basename($resource);
$existingCard = $this->backend->getCard($addressBookId, $cardUri);
if ($existingCard === false) {
$this->backend->createCard($addressBookId, $cardUri, $vCard);
} else {
$this->backend->updateCard($addressBookId, $cardUri, $vCard);
}
} else {
$this->backend->deleteCard($addressBookId, $cardUri);
}
}

Expand All @@ -90,6 +94,33 @@ public function syncRemoteAddressBook(string $url, string $userName, string $add
];
}

/**
* Download the vCards of the given resources, using addressbook-multiget when the remote supports it.
*
* @param list<string> $hrefs
* @return \Generator<string, string> vCard data keyed by href
*/
private function fetchCards(string $url, string $addressBookUrl, string $userName, string $sharedSecret, array $hrefs): \Generator {
$reportName = '{' . Plugin::NS_CARDDAV . '}addressbook-multiget';
if (!$this->requestReportSupport($addressBookUrl, $userName, $sharedSecret, $reportName)) {
foreach ($hrefs as $href) {
yield $href => $this->requestGet($this->prepareUri($url, $href), $userName, $sharedSecret);
}
return;
}

foreach (array_chunk($hrefs, self::MULTIGET_BATCH_SIZE) as $batch) {
yield from $this->requestMultiGet(
$addressBookUrl,
$userName,
$sharedSecret,
$batch,
$reportName,
'{' . Plugin::NS_CARDDAV . '}address-data',
);
}
}

/**
* @throws \Sabre\DAV\Exception\BadRequest
*/
Expand Down
142 changes: 141 additions & 1 deletion apps/dav/lib/Service/ASyncService.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
* Abstract sync service to sync CalDAV and CardDAV data from federated instances.
*/
abstract class ASyncService {
/** Maximum number of resources requested in one multiget REPORT */
protected const int MULTIGET_BATCH_SIZE = 100;

private ?IClient $client = null;

public function __construct(
Expand Down Expand Up @@ -105,7 +108,121 @@ protected function requestSyncReport(
return $this->parseMultiStatus($body, $absoluteUrl);
}

protected function download(
/**
* Fetch the data of several resources of a collection with one multiget REPORT.
*
* @param list<string> $hrefs Resource hrefs as returned by the sync report
* @param string $reportName Report element in clark notation, e.g. {urn:ietf:params:xml:ns:carddav}addressbook-multiget
* @param string $dataProperty Data property in clark notation, e.g. {urn:ietf:params:xml:ns:carddav}address-data
*
* @return array<string, string> Resource data keyed by href, resources the remote did not return are omitted
*/
protected function requestMultiGet(
string $absoluteUrl,
string $userName,
string $sharedSecret,
array $hrefs,
string $reportName,
string $dataProperty,
): array {
$client = $this->getClient();

$options = [
'auth' => [$userName, $sharedSecret],
'body' => $this->buildMultiGetRequestBody($hrefs, $reportName, $dataProperty),
'headers' => ['Content-Type' => 'application/xml'],
'timeout' => $this->config->getSystemValueInt(
'carddav_sync_request_timeout',
IClient::DEFAULT_REQUEST_TIMEOUT,
),
'verify' => !$this->config->getSystemValue(
'sharing.federation.allowSelfSignedCertificates',
false,
),
];

$response = $client->request(
'REPORT',
$absoluteUrl,
$options,
);

$body = $response->getBody();
assert(is_string($body));

/** @var MultiStatus $multiStatus */
$multiStatus = (new SabreXmlService())->expect('{DAV:}multistatus', $body);

$result = [];
foreach ($multiStatus->getResponses() as $resourceResponse) {
$data = $resourceResponse->getResponseProperties()[200][$dataProperty] ?? null;
if (is_string($data)) {
$result[$resourceResponse->getHref()] = $data;
}
}

return $result;
}

/**
* Check whether a collection supports a REPORT by reading its supported-report-set.
*
* @param string $reportName Report element in clark notation, e.g. {urn:ietf:params:xml:ns:carddav}addressbook-multiget
*/
protected function requestReportSupport(
string $absoluteUrl,
string $userName,
string $sharedSecret,
string $reportName,
): bool {
$client = $this->getClient();

$options = [
'auth' => [$userName, $sharedSecret],
'body' => '<?xml version="1.0" encoding="UTF-8"?><d:propfind xmlns:d="DAV:"><d:prop><d:supported-report-set/></d:prop></d:propfind>',
'headers' => ['Content-Type' => 'application/xml', 'Depth' => '0'],
'timeout' => $this->config->getSystemValueInt(
'carddav_sync_request_timeout',
IClient::DEFAULT_REQUEST_TIMEOUT,
),
'verify' => !$this->config->getSystemValue(
'sharing.federation.allowSelfSignedCertificates',
false,
),
];

$response = $client->request(
'PROPFIND',
$absoluteUrl,
$options,
);

$body = $response->getBody();
assert(is_string($body));

/** @var MultiStatus $multiStatus */
$multiStatus = (new SabreXmlService())->expect('{DAV:}multistatus', $body);

foreach ($multiStatus->getResponses() as $resourceResponse) {
$supportedReports = $resourceResponse->getResponseProperties()[200]['{DAV:}supported-report-set'] ?? [];
if (!is_array($supportedReports)) {
continue;
}
foreach ($supportedReports as $supportedReport) {
foreach ($supportedReport['value'] ?? [] as $report) {
foreach ($report['value'] ?? [] as $reportType) {
if (($reportType['name'] ?? null) === $reportName) {
return true;
}
}
}
}
}

return false;
}

protected function requestGet(
string $absoluteUrl,
string $userName,
string $sharedSecret,
Expand Down Expand Up @@ -145,6 +262,29 @@ private function buildSyncCollectionRequestBody(?string $syncToken): string {
return $dom->saveXML();
}

/**
* @param list<string> $hrefs
*/
private function buildMultiGetRequestBody(array $hrefs, string $reportName, string $dataProperty): string {
[$reportNamespace, $reportElement] = SabreXmlService::parseClarkNotation($reportName);
[$dataNamespace, $dataElement] = SabreXmlService::parseClarkNotation($dataProperty);

$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$root = $dom->createElementNS($reportNamespace, 'r:' . $reportElement);
$prop = $dom->createElementNS('DAV:', 'd:prop');
$prop->appendChild($dom->createElementNS('DAV:', 'd:getetag'));
$prop->appendChild($dom->createElementNS($dataNamespace, 'p:' . $dataElement));
$root->appendChild($prop);
foreach ($hrefs as $href) {
$hrefElement = $dom->createElementNS('DAV:', 'd:href');
$hrefElement->appendChild($dom->createTextNode($href));
$root->appendChild($hrefElement);
}
$dom->appendChild($root);
return $dom->saveXML();
}

/**
* @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
* @throws ParseException
Expand Down
Loading
Loading