From d16349ec1c450d9bd98748b3a56161ca85be55c7 Mon Sep 17 00:00:00 2001 From: Huseyn Ismayilov Date: Sat, 7 Mar 2026 16:00:05 +0100 Subject: [PATCH 1/2] Enhance getEvents with logging and error handling The issue was that the getEvents method was using propFind, which doesn't retrieve calendar event data correctly in CalDAV. I've updated it to use the proper REPORT method with a calendar-query to fetch all events from the calendar. This should now work with Zoho Calendar (and other CalDAV servers). The method sends a REPORT request to query for VEVENT components and extracts the calendar data from the XML response. Added logging for CalDAV events retrieval process and improved error handling for the REPORT request. --- Http/Helpers/CalDAV.php | 44 +++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/Http/Helpers/CalDAV.php b/Http/Helpers/CalDAV.php index 23ace07..6b92b37 100644 --- a/Http/Helpers/CalDAV.php +++ b/Http/Helpers/CalDAV.php @@ -19,17 +19,45 @@ public function __construct( $baseUri, $userName, $password ) { } public function getEvents( $calendarUrl ) { - $response = $this->client->propFind( $calendarUrl, [ - '{DAV:}displayname', - '{urn:ietf:params:xml:ns:caldav}calendar-description', - '{urn:ietf:params:xml:ns:caldav}calendar-data', - ], 2 ); + \Log::info('CalDAV getEvents called with URL', ['url' => $calendarUrl]); + + $xmlBody = '' . "\n" . + '' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ' ' . "\n" . + ''; + + $response = $this->client->request( 'REPORT', $calendarUrl, $xmlBody, [ + 'Content-Type' => 'application/xml; charset=utf-8', + 'Depth' => '1', + ] ); + + \Log::info('CalDAV REPORT response', ['status' => $response['statusCode'] ?? 'no status', 'body_length' => strlen($response['body'] ?? '')]); $events = []; - foreach ( $response as $eventData ) { - if ( isset( $eventData['{urn:ietf:params:xml:ns:caldav}calendar-data'] ) ) { - $events[] = $eventData['{urn:ietf:params:xml:ns:caldav}calendar-data']; + if ( isset( $response['statusCode'] ) && $response['statusCode'] >= 200 && $response['statusCode'] < 300 ) { + if ( isset( $response['body'] ) && ! empty( $response['body'] ) ) { + \Log::info('CalDAV response body preview', ['body' => substr($response['body'], 0, 500)]); + // Parse the XML response to extract calendar-data + $matches = []; + preg_match_all( '/<(?:C:)?calendar-data[^>]*>(.*?)<\/(?:C:)?calendar-data>/s', $response['body'], $matches ); + if ( isset( $matches[1] ) ) { + foreach ( $matches[1] as $data ) { + $events[] = html_entity_decode( $data, ENT_XML1, 'UTF-8' ); + } + } + \Log::info('CalDAV parsed events count', ['count' => count($events)]); } + } else { + \Log::error('CalDAV REPORT failed', ['status' => $response['statusCode'] ?? 'unknown', 'body' => $response['body'] ?? 'no body']); } return $events; From db961ee8a0f3d3e9c05357ab72470765ee8a4f31 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 11 Mar 2026 23:45:35 +0100 Subject: [PATCH 2/2] Fix CalDAV Zoho event retrieval fallback. Handle REPORT responses without calendar-data by fetching each event .ics via href, reduce noisy/sensitive logging, and preserve ICS separators so imported events parse correctly. --- Entities/Calendar.php | 3 +- Http/Helpers/CalDAV.php | 163 ++++++++++++++++++++++++++++++++++------ 2 files changed, 140 insertions(+), 26 deletions(-) diff --git a/Entities/Calendar.php b/Entities/Calendar.php index 4d5d8d1..39a0537 100644 --- a/Entities/Calendar.php +++ b/Entities/Calendar.php @@ -131,7 +131,8 @@ public function getExternalContent( bool $force = false ): ?string { $remainingUrl = substr( $fullUrl, strpos( $fullUrl, '/', 8 ) ); $caldavClient = new CalDAV( $baseUrl, $this->custom_fields['username'], $this->custom_fields['password'] ); - $data = implode( '', $caldavClient->getEvents( $remainingUrl ) ); + // Keep event payloads separated to preserve valid ICS line boundaries. + $data = implode( "\r\n", $caldavClient->getEvents( $remainingUrl ) ); } catch ( Exception $e ) { Log::error( $e->getMessage(), [ 'exception' => $e ] ); diff --git a/Http/Helpers/CalDAV.php b/Http/Helpers/CalDAV.php index 6b92b37..ea76aa5 100644 --- a/Http/Helpers/CalDAV.php +++ b/Http/Helpers/CalDAV.php @@ -19,7 +19,7 @@ public function __construct( $baseUri, $userName, $password ) { } public function getEvents( $calendarUrl ) { - \Log::info('CalDAV getEvents called with URL', ['url' => $calendarUrl]); + \Log::debug( 'CalDAV getEvents called' ); $xmlBody = '' . "\n" . '' . "\n" . @@ -35,29 +35,35 @@ public function getEvents( $calendarUrl ) { ' ' . "\n" . ''; - $response = $this->client->request( 'REPORT', $calendarUrl, $xmlBody, [ - 'Content-Type' => 'application/xml; charset=utf-8', - 'Depth' => '1', - ] ); + try { + $response = $this->client->request( 'REPORT', $calendarUrl, $xmlBody, [ + 'Content-Type' => 'application/xml; charset=utf-8', + 'Depth' => '1', + ] ); + } catch ( \Exception $e ) { + \Log::debug( 'CalDAV REPORT query failed, returning empty events list', [ + 'error' => $e->getMessage(), + ] ); + return []; + } - \Log::info('CalDAV REPORT response', ['status' => $response['statusCode'] ?? 'no status', 'body_length' => strlen($response['body'] ?? '')]); + \Log::debug( 'CalDAV REPORT response', [ + 'status' => $response['statusCode'] ?? 'no status', + 'body_length' => strlen( $response['body'] ?? '' ), + ] ); $events = []; if ( isset( $response['statusCode'] ) && $response['statusCode'] >= 200 && $response['statusCode'] < 300 ) { if ( isset( $response['body'] ) && ! empty( $response['body'] ) ) { - \Log::info('CalDAV response body preview', ['body' => substr($response['body'], 0, 500)]); - // Parse the XML response to extract calendar-data - $matches = []; - preg_match_all( '/<(?:C:)?calendar-data[^>]*>(.*?)<\/(?:C:)?calendar-data>/s', $response['body'], $matches ); - if ( isset( $matches[1] ) ) { - foreach ( $matches[1] as $data ) { - $events[] = html_entity_decode( $data, ENT_XML1, 'UTF-8' ); - } + $events = $this->extractCalendarDataFromMultistatus( $response['body'] ); + if ( count( $events ) === 0 ) { + $hrefs = $this->extractEventHrefsFromMultistatus( $response['body'], $calendarUrl ); + $events = $this->fetchEventsByHref( $hrefs ); } - \Log::info('CalDAV parsed events count', ['count' => count($events)]); + \Log::debug( 'CalDAV parsed events count', [ 'count' => count( $events ) ] ); } } else { - \Log::error('CalDAV REPORT failed', ['status' => $response['statusCode'] ?? 'unknown', 'body' => $response['body'] ?? 'no body']); + \Log::debug( 'CalDAV REPORT failed', [ 'status' => $response['statusCode'] ?? 'unknown' ] ); } return $events; @@ -128,7 +134,7 @@ public function deleteEvent( $calendarUrl, $uid ) { /** * Get a single event by UID using CalDAV REPORT query * This is much more efficient than fetching all events - * + * * @param string $calendarUrl The calendar URL * @param string $uid The event UID to fetch * @return array|null The event data or null if not found @@ -185,25 +191,25 @@ public function getEventByUid( $calendarUrl, $uid ) { /** * Check if the CalDAV server supports REPORT queries - * + * * @param string $calendarUrl * @return bool */ public function supportsReportQuery( $calendarUrl ) { try { $response = $this->client->options( $calendarUrl ); - + if ( isset( $response['headers']['allow'] ) ) { - $allow = is_array( $response['headers']['allow'] ) - ? implode( ',', $response['headers']['allow'] ) + $allow = is_array( $response['headers']['allow'] ) + ? implode( ',', $response['headers']['allow'] ) : $response['headers']['allow']; return stripos( $allow, 'REPORT' ) !== false; } - + // Also check DAV header for calendar-access if ( isset( $response['headers']['dav'] ) ) { - $dav = is_array( $response['headers']['dav'] ) - ? implode( ',', $response['headers']['dav'] ) + $dav = is_array( $response['headers']['dav'] ) + ? implode( ',', $response['headers']['dav'] ) : $response['headers']['dav']; return stripos( $dav, 'calendar-access' ) !== false; } @@ -213,8 +219,115 @@ public function supportsReportQuery( $calendarUrl ) { 'calendar_url' => $calendarUrl ] ); } - + return false; } + /** + * Extract calendar-data nodes from a CalDAV multistatus XML response. + * Uses XML parsing for namespace-agnostic extraction and regex as fallback. + * + * @param string $responseBody + * @return string[] + */ + private function extractCalendarDataFromMultistatus( $responseBody ) { + $events = []; + + libxml_use_internal_errors( true ); + $document = new \DOMDocument(); + if ( $document->loadXML( $responseBody ) ) { + $xpath = new \DOMXPath( $document ); + $nodes = $xpath->query( '//*[local-name()="calendar-data"]' ); + if ( $nodes !== false ) { + foreach ( $nodes as $node ) { + $data = trim( $node->nodeValue ); + if ( $data !== '' ) { + $events[] = html_entity_decode( $data, ENT_XML1, 'UTF-8' ); + } + } + } + } + libxml_clear_errors(); + + if ( count( $events ) > 0 ) { + return $events; + } + + $matches = []; + preg_match_all( '/<(?:[A-Za-z0-9_-]+:)?calendar-data[^>]*>(.*?)<\/(?:[A-Za-z0-9_-]+:)?calendar-data>/s', $responseBody, $matches ); + if ( isset( $matches[1] ) ) { + foreach ( $matches[1] as $data ) { + $decoded = html_entity_decode( trim( $data ), ENT_XML1, 'UTF-8' ); + if ( $decoded !== '' ) { + $events[] = $decoded; + } + } + } + + return $events; + } + + /** + * Extract event hrefs from a CalDAV multistatus XML response. + * + * @param string $responseBody + * @param string $calendarUrl + * @return string[] + */ + private function extractEventHrefsFromMultistatus( $responseBody, $calendarUrl ) { + $hrefs = []; + + libxml_use_internal_errors( true ); + $document = new \DOMDocument(); + if ( $document->loadXML( $responseBody ) ) { + $xpath = new \DOMXPath( $document ); + $nodes = $xpath->query( '//*[local-name()="href"]' ); + if ( $nodes !== false ) { + foreach ( $nodes as $node ) { + $href = trim( $node->nodeValue ); + if ( $href === '' ) { + continue; + } + if ( substr( $href, -1 ) === '/' ) { + continue; + } + if ( stripos( $href, '.ics' ) === false ) { + continue; + } + $hrefs[] = $href; + } + } + } + libxml_clear_errors(); + + return array_values( array_unique( $hrefs ) ); + } + + /** + * Fetch event bodies by href list. + * + * @param string[] $hrefs + * @return string[] + */ + private function fetchEventsByHref( $hrefs ) { + $events = []; + foreach ( $hrefs as $href ) { + try { + $response = $this->client->request( 'GET', $href, null, [ + 'Accept' => 'text/calendar, */*;q=0.1', + ] ); + if ( isset( $response['statusCode'] ) && $response['statusCode'] >= 200 && $response['statusCode'] < 300 ) { + $body = trim( $response['body'] ?? '' ); + if ( $body !== '' ) { + $events[] = $body; + } + } + } catch ( \Exception $e ) { + \Log::debug( 'CalDAV event GET failed', [ 'error' => $e->getMessage() ] ); + } + } + + return $events; + } + }