fetchCardsWithReport(); if ($cards !== []) { return $cards; } } catch (GuzzleException|RuntimeException) { // Some CardDAV servers restrict addressbook-query. Fall back to PROPFIND + GET. } return $this->fetchCardsWithPropfind(); } /** * @return array */ private function fetchCardsWithReport(): array { $url = rtrim((string) config('relais_sms.carddav.url'), '/').'/'; $username = (string) config('relais_sms.carddav.username'); $password = (string) config('relais_sms.carddav.password'); $this->ensureConfigured($url, $username, $password); $response = $this->http->request('REPORT', $url, [ 'auth' => [$username, $password], 'headers' => [ 'Content-Type' => 'application/xml; charset=utf-8', 'Depth' => '1', ], 'body' => <<<'XML' XML, 'timeout' => (float) config('relais_sms.carddav.timeout'), ]); return $this->parseReport((string) $response->getBody()); } /** * @return array */ private function fetchCardsWithPropfind(): array { $url = rtrim((string) config('relais_sms.carddav.url'), '/').'/'; $username = (string) config('relais_sms.carddav.username'); $password = (string) config('relais_sms.carddav.password'); $this->ensureConfigured($url, $username, $password); $response = $this->http->request('PROPFIND', $url, [ 'auth' => [$username, $password], 'headers' => [ 'Content-Type' => 'application/xml; charset=utf-8', 'Depth' => '1', ], 'body' => <<<'XML' XML, 'timeout' => (float) config('relais_sms.carddav.timeout'), ]); $entries = $this->parsePropfind((string) $response->getBody(), $url); $cards = []; foreach ($entries as $entry) { $cardResponse = $this->http->request('GET', $entry['url'], [ 'auth' => [$username, $password], 'headers' => [ 'Accept' => 'text/vcard,text/x-vcard,text/plain,*/*', ], 'timeout' => (float) config('relais_sms.carddav.timeout'), ]); $cards[] = new CardDavRemoteCard( href: $entry['href'], etag: $entry['etag'], vcard: (string) $cardResponse->getBody(), ); } return $cards; } private function ensureConfigured(string $url, string $username, string $password): void { if ($url === '/' || $username === '' || $password === '') { throw new RuntimeException('Configuration CardDAV incomplete.'); } } /** * @return array */ private function parseReport(string $xml): array { try { $document = new SimpleXMLElement($xml); } catch (Throwable $exception) { throw new RuntimeException('Reponse CardDAV REPORT illisible.', previous: $exception); } $document->registerXPathNamespace('d', 'DAV:'); $responses = $document->xpath('//d:response') ?: []; $cards = []; foreach ($responses as $response) { $response->registerXPathNamespace('d', 'DAV:'); $response->registerXPathNamespace('c', 'urn:ietf:params:xml:ns:carddav'); $hrefNodes = $response->xpath('./d:href') ?: []; $cardNodes = $response->xpath('.//c:address-data') ?: []; $href = trim((string) ($hrefNodes[0] ?? '')); $vcard = (string) ($cardNodes[0] ?? ''); if ($href === '' || trim($vcard) === '') { continue; } $etagNodes = $response->xpath('.//d:getetag') ?: []; $cards[] = new CardDavRemoteCard( href: $href, etag: trim((string) ($etagNodes[0] ?? ''), '"') ?: null, vcard: $vcard, ); } return $cards; } /** * @return array */ private function parsePropfind(string $xml, string $baseUrl): array { try { $document = new SimpleXMLElement($xml); } catch (Throwable $exception) { throw new RuntimeException('Reponse CardDAV PROPFIND illisible.', previous: $exception); } $document->registerXPathNamespace('d', 'DAV:'); $responses = $document->xpath('//d:response') ?: []; $entries = []; foreach ($responses as $response) { $response->registerXPathNamespace('d', 'DAV:'); $hrefNodes = $response->xpath('./d:href') ?: []; $href = trim((string) ($hrefNodes[0] ?? '')); if ($href === '' || ! str_ends_with(strtolower(parse_url($href, PHP_URL_PATH) ?: $href), '.vcf')) { continue; } $etagNodes = $response->xpath('.//d:getetag') ?: []; $entries[] = [ 'href' => $href, 'etag' => trim((string) ($etagNodes[0] ?? ''), '"') ?: null, 'url' => $this->absoluteUrl($baseUrl, $href), ]; } return $entries; } private function absoluteUrl(string $baseUrl, string $href): string { if (parse_url($href, PHP_URL_SCHEME)) { return $href; } $base = parse_url($baseUrl); if (! isset($base['scheme'], $base['host'])) { throw new RuntimeException('URL CardDAV invalide.'); } $port = isset($base['port']) ? ':'.$base['port'] : ''; $path = str_starts_with($href, '/') ? $href : rtrim((string) ($base['path'] ?? ''), '/').'/'.$href; return $base['scheme'].'://'.$base['host'].$port.$path; } }