*/ public function fetchEvents(CarbonInterface $from, CarbonInterface $to): array { try { $events = $this->fetchEventsWithReport($from, $to); if ($events !== []) { return $events; } } catch (GuzzleException|RuntimeException) { // Some CalDAV servers restrict calendar-query. Fall back to PROPFIND + GET. } return $this->fetchEventsWithPropfind(); } /** * @return array */ private function fetchEventsWithReport(CarbonInterface $from, CarbonInterface $to): array { $url = rtrim((string) config('relais_sms.caldav.url'), '/').'/'; $username = (string) config('relais_sms.caldav.username'); $password = (string) config('relais_sms.caldav.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' => $this->calendarQueryBody($from, $to), 'timeout' => (float) config('relais_sms.caldav.timeout'), ]); return $this->parseReport((string) $response->getBody()); } /** * @return array */ private function fetchEventsWithPropfind(): array { $url = rtrim((string) config('relais_sms.caldav.url'), '/').'/'; $username = (string) config('relais_sms.caldav.username'); $password = (string) config('relais_sms.caldav.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.caldav.timeout'), ]); $entries = $this->parsePropfind((string) $response->getBody(), $url); $events = []; foreach ($entries as $entry) { $eventResponse = $this->http->request('GET', $entry['url'], [ 'auth' => [$username, $password], 'headers' => [ 'Accept' => 'text/calendar,text/plain,*/*', ], 'timeout' => (float) config('relais_sms.caldav.timeout'), ]); $events[] = new CalendarRemoteEvent( href: $entry['href'], etag: $entry['etag'], ical: (string) $eventResponse->getBody(), ); } return $events; } private function ensureConfigured(string $url, string $username, string $password): void { if ($url === '/' || $username === '' || $password === '') { throw new RuntimeException('Configuration CalDAV agenda incomplete.'); } } private function calendarQueryBody(CarbonInterface $from, CarbonInterface $to): string { $start = $from->copy()->utc()->format('Ymd\THis\Z'); $end = $to->copy()->utc()->format('Ymd\THis\Z'); return << XML; } /** * @return array */ private function parseReport(string $xml): array { try { $document = new SimpleXMLElement($xml); } catch (Throwable $exception) { throw new RuntimeException('Reponse CalDAV REPORT illisible.', previous: $exception); } $document->registerXPathNamespace('d', 'DAV:'); $responses = $document->xpath('//d:response') ?: []; $events = []; foreach ($responses as $response) { $response->registerXPathNamespace('d', 'DAV:'); $response->registerXPathNamespace('c', 'urn:ietf:params:xml:ns:caldav'); $hrefNodes = $response->xpath('./d:href') ?: []; $calendarDataNodes = $response->xpath('.//c:calendar-data') ?: []; $href = trim((string) ($hrefNodes[0] ?? '')); $ical = (string) ($calendarDataNodes[0] ?? ''); if ($href === '' || trim($ical) === '') { continue; } $etagNodes = $response->xpath('.//d:getetag') ?: []; $events[] = new CalendarRemoteEvent( href: $href, etag: trim((string) ($etagNodes[0] ?? ''), '"') ?: null, ical: $ical, ); } return $events; } /** * @return array */ private function parsePropfind(string $xml, string $baseUrl): array { try { $document = new SimpleXMLElement($xml); } catch (Throwable $exception) { throw new RuntimeException('Reponse CalDAV 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), '.ics')) { 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 CalDAV agenda 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; } }