From 1766dd775f02f2f5a4f7b072844c66d451a52319 Mon Sep 17 00:00:00 2001 From: stephan1827 Date: Thu, 25 Jun 2026 18:23:56 +0200 Subject: [PATCH] fix: handle undeclared XML namespace prefixes in PROPFIND responses Fastmail (Cyrus IMAP backend) includes proprietary elements such as CY:write-properties-resource in CalDAV PROPFIND responses without declaring the CY namespace prefix. This causes Sabre's strict XML parser to throw a LibXMLException before any standard DAV properties can be extracted. On ParseException, collect all namespace prefixes used in the response XML that have no corresponding xmlns declaration, inject placeholder URN declarations into the root element, then retry parsing. The placeholder namespaces are ignored by the harmonization logic since only well-known DAV/CalDAV/CardDAV properties are consumed. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: stephan1827 --- lib/Service/Remote/RemoteClient.php | 33 +++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/Service/Remote/RemoteClient.php b/lib/Service/Remote/RemoteClient.php index 50efe40..8322e87 100644 --- a/lib/Service/Remote/RemoteClient.php +++ b/lib/Service/Remote/RemoteClient.php @@ -563,8 +563,16 @@ private function parseMultistatusProperties(IResponse $response): array { return []; } - /** @var MultiStatus $multistatus */ - $multistatus = (new SabreXmlService())->expect(self::DAV_MULTISTATUS, $body); + try { + $multistatus = (new SabreXmlService())->expect(self::DAV_MULTISTATUS, $body); + } catch (ParseException $e) { + // Some servers (e.g. Fastmail/Cyrus IMAP) return XML containing namespace + // prefixes (e.g. CY:write-properties-resource) that are never declared with + // an xmlns attribute. Inject placeholder declarations so Sabre can parse the + // standard DAV properties we actually need, then retry. + $body = $this->declareUndeclaredNamespacePrefixes($body); + $multistatus = (new SabreXmlService())->expect(self::DAV_MULTISTATUS, $body); + } $result = []; foreach ($multistatus->getResponses() as $davResponse) { @@ -577,6 +585,27 @@ private function parseMultistatusProperties(IResponse $response): array { return $result; } + private function declareUndeclaredNamespacePrefixes(string $xml): string { + preg_match_all('/<(?![\?!\/])([a-zA-Z][a-zA-Z0-9_-]*):/', $xml, $usedMatches); + $usedPrefixes = array_unique(array_filter($usedMatches[1])); + + preg_match_all('/\bxmlns:([a-zA-Z][a-zA-Z0-9_-]*)\s*=/', $xml, $declaredMatches); + $declaredPrefixes = array_flip($declaredMatches[1]); + + $extra = ''; + foreach ($usedPrefixes as $prefix) { + if (!isset($declaredPrefixes[$prefix])) { + $extra .= sprintf(' xmlns:%s="urn:unknown-ns:%s"', $prefix, $prefix); + } + } + + if ($extra === '') { + return $xml; + } + + return preg_replace('/(<[a-zA-Z:][^>]*?)(\s*\/?>)/', '$1' . $extra . '$2', $xml, 1); + } + /** * @param array> $responseProperties */