Incorrect Timezone Handling in CalDAV Synchronization Causes Time Shifts (#1626)

This commit is contained in:
Alex Tselegidis 2024-12-19 21:01:47 +02:00
parent b2ed516b5f
commit 8e14176e52
2 changed files with 65 additions and 28 deletions

View file

@ -17,6 +17,7 @@ developers to maintain and readjust their custom modifications on the main proje
- Fix various 1.5.0 API issues (#1562) - Fix various 1.5.0 API issues (#1562)
- Correct email issues by replacing the internal email library with phpmailer (#1587) - Correct email issues by replacing the internal email library with phpmailer (#1587)
- Fix ICS file mimetype (#1630) - Fix ICS file mimetype (#1630)
- Incorrect Timezone Handling in CalDAV Synchronization Causes Time Shifts (#1626)

View file

@ -457,6 +457,37 @@ class Caldav_sync
return str_replace('METHOD:PUBLISH', '', $ics_file); return str_replace('METHOD:PUBLISH', '', $ics_file);
} }
/**
* Try to parse the CalDAV event date-time value with the right timezone.
*
* @throws DateMalformedStringException
* @throws DateInvalidTimeZoneException
*/
private function parse_date_time_object(string $caldav_date_time, DateTimeZone $default_timezone_object): DateTime
{
try {
if (str_contains($caldav_date_time, 'TZID=')) {
// Extract the TZID and use it
preg_match('/TZID=([^:]+):/', $caldav_date_time, $matches);
$parsed_timezone = $matches[1];
$parsed_timezone_object = new DateTimeZone($parsed_timezone);
$date_time = preg_replace('/TZID=[^:]+:/', '', $caldav_date_time);
$date_time_object = new DateTime($date_time, $parsed_timezone_object);
} elseif (str_ends_with($caldav_date_time, 'Z')) {
// Handle UTC timestamps
$date_time_object = new DateTime($caldav_date_time, new DateTimeZone('UTC'));
} else {
// Default to the provided timezone
$date_time_object = new DateTime($caldav_date_time, $default_timezone_object);
}
return $date_time_object;
} catch (Throwable $e) {
error_log('Error parsing date-time value (' . $caldav_date_time . ') with timezone: ' . $e->getMessage());
throw $e;
}
}
/** /**
* Convert the VEvent object to an associative array * Convert the VEvent object to an associative array
* *
@ -467,45 +498,50 @@ class Caldav_sync
* *
* @return array * @return array
* *
* @throws DateMalformedStringException * @throws Throwable
*/ */
private function convert_caldav_event_to_array_event(VEvent $vevent, DateTimeZone $timezone_object): array private function convert_caldav_event_to_array_event(VEvent $vevent, DateTimeZone $timezone_object): array
{ {
$utc_timezone_object = new DateTimeZone('UTC'); // Convert from UTC to local provider timezone try {
$caldav_start_date_time = (string) $vevent->DTSTART;
$start_date_time_object = $this->parse_date_time_object($caldav_start_date_time, $timezone_object);
$start_date_time_object->setTimezone($timezone_object); // Convert to the provider timezone
$start_date_time_object = new DateTime((string) $vevent->DTSTART, $utc_timezone_object); $caldav_end_date_time = (string) $vevent->DTEND;
$start_date_time_object->setTimezone($timezone_object); $end_date_time_object = $this->parse_date_time_object($caldav_end_date_time, $timezone_object);
$end_date_time_object->setTimezone($timezone_object); // Convert to the provider timezone
$end_date_time_object = new DateTime((string) $vevent->DTEND, $utc_timezone_object); // Check if the event is recurring
$end_date_time_object->setTimezone($timezone_object);
// Check if the event is recurring $is_recurring_event =
isset($vevent->RRULE) ||
isset($vevent->RDATE) ||
isset($vevent->{'RECURRENCE-ID'}) ||
isset($vevent->EXDATE);
$is_recurring_event = // Generate ID based on recurrence status
isset($vevent->RRULE) ||
isset($vevent->RDATE) ||
isset($vevent->{'RECURRENCE-ID'}) ||
isset($vevent->EXDATE);
// Generate ID based on recurrence status $event_id = (string) $vevent->UID;
$event_id = (string) $vevent->UID; if ($is_recurring_event) {
$event_id .= '-RECURRENCE-' . random_string();
}
if ($is_recurring_event) { // Return the converted event
$event_id .= '-RECURRENCE-' . random_string();
return [
'id' => $event_id,
'summary' => (string) $vevent->SUMMARY ?? null ?: '',
'start_datetime' => $start_date_time_object->format('Y-m-d H:i:s'),
'end_datetime' => $end_date_time_object->format('Y-m-d H:i:s'),
'description' => (string) $vevent->DESCRIPTION ?? null ?: '',
'status' => (string) $vevent->STATUS ?? null ?: 'CONFIRMED',
'location' => (string) $vevent->LOCATION ?? null ?: '',
];
} catch (Throwable $e) {
error_log('Error parsing CalDAV event object (' . var_export($vevent, true) . '): ' . $e->getMessage());
throw $e;
} }
// Return the converted event
return [
'id' => $event_id,
'summary' => (string) $vevent->SUMMARY,
'start_datetime' => $start_date_time_object->format('Y-m-d H:i:s'),
'end_datetime' => $end_date_time_object->format('Y-m-d H:i:s'),
'description' => (string) $vevent->DESCRIPTION,
'status' => (string) $vevent->STATUS,
'location' => (string) $vevent->LOCATION,
];
} }
/** /**