HEX
Server: LiteSpeed
System: Linux d8 4.18.0-553.121.1.lve.el8.x86_64 #1 SMP Thu Apr 30 16:40:41 UTC 2026 x86_64
User: wbwebdes (3015)
PHP: 8.1.31
Disabled: exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Upload Files
File: /home/wbwebdes/domains/files.wb-cloud.nl/public_html/apps/dav/lib/Service/ASyncService.php
<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OCA\DAV\Service;

use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use Sabre\DAV\Xml\Response\MultiStatus;
use Sabre\DAV\Xml\Service as SabreXmlService;
use Sabre\Xml\ParseException;

/**
 * Abstract sync service to sync CalDAV and CardDAV data from federated instances.
 */
abstract class ASyncService {
	private ?IClient $client = null;

	public function __construct(
		protected IClientService $clientService,
		protected IConfig $config,
	) {
	}

	private function getClient(): IClient {
		if ($this->client === null) {
			$this->client = $this->clientService->newClient();
		}

		return $this->client;
	}

	protected function prepareUri(string $host, string $path): string {
		/*
		 * The trailing slash is important for merging the uris together.
		 *
		 * $host is stored in oc_trusted_servers.url and usually without a trailing slash.
		 *
		 * Example for a report request
		 *
		 * $host = 'https://server.internal/cloud'
		 * $path = 'remote.php/dav/addressbooks/system/system/system'
		 *
		 * Without the trailing slash, the webroot is missing:
		 * https://server.internal/remote.php/dav/addressbooks/system/system/system
		 *
		 * Example for a download request
		 *
		 * $host = 'https://server.internal/cloud'
		 * $path = '/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf'
		 *
		 * The response from the remote usually contains the webroot already and must be normalized to:
		 * https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf
		 */
		$host = rtrim($host, '/') . '/';

		$uri = \GuzzleHttp\Psr7\UriResolver::resolve(
			\GuzzleHttp\Psr7\Utils::uriFor($host),
			\GuzzleHttp\Psr7\Utils::uriFor($path)
		);

		return (string)$uri;
	}

	/**
	 * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
	 */
	protected function requestSyncReport(
		string $absoluteUrl,
		string $userName,
		string $sharedSecret,
		?string $syncToken,
	): array {
		$client = $this->getClient();

		$options = [
			'auth' => [$userName, $sharedSecret],
			'body' => $this->buildSyncCollectionRequestBody($syncToken),
			'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));

		return $this->parseMultiStatus($body, $absoluteUrl);
	}

	protected function download(
		string $absoluteUrl,
		string $userName,
		string $sharedSecret,
	): string {
		$client = $this->getClient();

		$options = [
			'auth' => [$userName, $sharedSecret],
			'verify' => !$this->config->getSystemValue(
				'sharing.federation.allowSelfSignedCertificates',
				false,
			),
		];

		$response = $client->get(
			$absoluteUrl,
			$options,
		);

		return (string)$response->getBody();
	}

	private function buildSyncCollectionRequestBody(?string $syncToken): string {
		$dom = new \DOMDocument('1.0', 'UTF-8');
		$dom->formatOutput = true;
		$root = $dom->createElementNS('DAV:', 'd:sync-collection');
		$sync = $dom->createElement('d:sync-token', $syncToken ?? '');
		$prop = $dom->createElement('d:prop');
		$cont = $dom->createElement('d:getcontenttype');
		$etag = $dom->createElement('d:getetag');

		$prop->appendChild($cont);
		$prop->appendChild($etag);
		$root->appendChild($sync);
		$root->appendChild($prop);
		$dom->appendChild($root);
		return $dom->saveXML();
	}

	/**
	 * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool}
	 * @throws ParseException
	 */
	private function parseMultiStatus(string $body, string $resourceUrl): array {
		/** @var MultiStatus $multiStatus */
		$multiStatus = (new SabreXmlService())->expect('{DAV:}multistatus', $body);

		$result = [];
		$truncated = false;

		foreach ($multiStatus->getResponses() as $response) {
			$href = $response->getHref();
			if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $resourceUrl)) {
				$truncated = true;
			} else {
				$result[$response->getHref()] = $response->getResponseProperties();
			}
		}

		return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated];
	}

	/**
	 * Determines whether the provided response URI corresponds to the given request URI.
	 */
	private function isResponseForRequestUri(string $responseUri, string $requestUri): bool {
		/*
		 * Example response uri:
		 *
		 * /remote.php/dav/addressbooks/system/system/system/
		 * /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory)
		 *
		 * Example request uri:
		 *
		 * https://foo.bar/remote.php/dav/addressbooks/system/system/system
		 *
		 * References:
		 * https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174
		 * https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41
		 */
		return str_ends_with(
			rtrim($requestUri, '/'),
			rtrim($responseUri, '/'),
		);
	}
}