diff --git a/ics-collector/lib/EventObject.php b/ics-collector/lib/EventObject.php index ad6623c..bcbded2 100644 --- a/ics-collector/lib/EventObject.php +++ b/ics-collector/lib/EventObject.php @@ -27,6 +27,10 @@ class EventObject public $categories; public $xWrSource; public $xWrSourceUrl; + + // Array properties to store timezone info + public $dtstart_array; + public $dtend_array; public function __construct($data = array()) { @@ -37,10 +41,9 @@ public function __construct($data = array()) } else { $variable = $key; } - $this->{$variable} = $value; - + if (is_array($value)) { - $this->{$variable} = $value; + $this->{$variable} = $value; } else { $this->{$variable} = stripslashes(trim(str_replace('\n', "\n", $value))); } @@ -56,17 +59,55 @@ public static function __set_state($anArray) /** * Return Event data excluding anything blank - * as ICS format + * as ICS format with proper timezone handling * * @return string */ public function printIcs() { $crlf = "\r\n"; + $output = "BEGIN:VEVENT".$crlf; + + // Get default timezone + $defaultTimezone = 'Europe/Berlin'; + + // Handle DTSTART with timezone information + if (!empty($this->dtstart)) { + // Skip values that end with Z (already in UTC) + $isUtc = (substr($this->dtstart, -1) === 'Z'); + + if (isset($this->dtstart_array) && isset($this->dtstart_array[0]['TZID'])) { + // Use timezone from the event data + $output .= sprintf("DTSTART;TZID=%s:%s%s", $this->dtstart_array[0]['TZID'], $this->dtstart, $crlf); + } elseif (!$isUtc && preg_match("/^\d{8}T\d{6}/", $this->dtstart)) { + // Add default timezone for datetime values without timezone + $output .= sprintf("DTSTART;TZID=%s:%s%s", $defaultTimezone, $this->dtstart, $crlf); + } else { + // Keep all-day events and UTC times as is + $output .= sprintf("DTSTART:%s%s", $this->dtstart, $crlf); + } + } + + // Handle DTEND with timezone information + if (!empty($this->dtend)) { + // Skip values that end with Z (already in UTC) + $isUtc = (substr($this->dtend, -1) === 'Z'); + + if (isset($this->dtend_array) && isset($this->dtend_array[0]['TZID'])) { + // Use timezone from the event data + $output .= sprintf("DTEND;TZID=%s:%s%s", $this->dtend_array[0]['TZID'], $this->dtend, $crlf); + } elseif (!$isUtc && preg_match("/^\d{8}T\d{6}/", $this->dtend)) { + // Add default timezone for datetime values without timezone + $output .= sprintf("DTEND;TZID=%s:%s%s", $defaultTimezone, $this->dtend, $crlf); + } else { + // Keep all-day events and UTC times as is + $output .= sprintf("DTEND:%s%s", $this->dtend, $crlf); + } + } + + // Process other properties $data = array( 'SUMMARY' => $this->summary, - 'DTSTART' => $this->dtstart, - 'DTEND' => $this->dtend, 'DURATION' => $this->duration, 'DTSTAMP' => $this->dtstamp, 'UID' => $this->uid, @@ -86,7 +127,6 @@ public function printIcs() $data = array_map('trim', $data); // Trim all values $data = array_filter($data); // Remove any blank values - $output = "BEGIN:VEVENT".$crlf; foreach ($data as $key => $value) { $output .= sprintf("%s:%s%s", $key, $value, $crlf); diff --git a/ics-collector/lib/ics-merger.php b/ics-collector/lib/ics-merger.php index 9350779..64855ce 100644 --- a/ics-collector/lib/ics-merger.php +++ b/ics-collector/lib/ics-merger.php @@ -149,16 +149,37 @@ private function processEvents($events, $timezone = null) { $event['DTSTAMP'] = $event['DTSTAMP'] . "Z"; } case 'DTSTART': - if (!preg_match("/^\d{8}T\d{6}/", $event['DTSTART']) && - preg_match("/^\d{8}$/", $event['DTSTART'])) { - $event['DTSTART'] = $event['DTSTART'] . "T000000Z"; + // Check if DTSTART already has a TZID parameter + if (strpos($value, 'TZID=') === false) { + // Add timezone parameter for date-time values (not for all-day events with just date) + if (preg_match("/^\d{8}T\d{6}/", $value) && substr($value, -1) !== 'Z') { + // Use event timezone, calendar timezone, or default timezone + $tzid = $timezone ?? $this->defaultHeader['X-WR-TIMEZONE']; + $event[$key] = 'TZID=' . $tzid . ':' . $value; + } + // For all-day events and UTC times, keep as is + else if (preg_match("/^\d{8}$/", $value) || substr($value, -1) === 'Z') { + $event[$key] = $value; + } } - case 'DTEND' : - if (array_key_exists("DTEND", $event) && - !preg_match("/^\d{8}T\d{6}/", $event['DTEND']) && - preg_match("/^\d{8}$/", $event['DTEND'])) { - $event['DTEND'] = $event['DTEND'] . "T000000Z"; + break; + case 'DTEND': + if (array_key_exists("DTEND", $event)) { + // Check if DTEND already has a TZID parameter + if (strpos($value, 'TZID=') === false) { + // Add timezone parameter for date-time values (not for all-day events with just date) + if (preg_match("/^\d{8}T\d{6}/", $value) && substr($value, -1) !== 'Z') { + // Use event timezone, calendar timezone, or default timezone + $tzid = $timezone ?? $this->defaultHeader['X-WR-TIMEZONE']; + $event[$key] = 'TZID=' . $tzid . ':' . $value; + } + // For all-day events and UTC times, keep as is + else if (preg_match("/^\d{8}$/", $value) || substr($value, -1) === 'Z') { + $event[$key] = $value; + } + } } + break; case 'LAST-MODIFIED': if (array_key_exists("LAST-MODIFIED", $event) && preg_match("/^\d{8}T\d{6}$/", $event['LAST-MODIFIED']) ) { @@ -198,6 +219,41 @@ private function processEvents($events, $timezone = null) { return $events; } + /** + * Generate VTIMEZONE block with proper DST rules + * @param string $timezone Timezone identifier + * @return string Generated VTIMEZONE block + */ + private static function generateVTimeZone($timezone = 'Europe/Berlin') { + $str = "BEGIN:VTIMEZONE\r\n"; + $str .= "TZID:" . $timezone . "\r\n"; + $str .= "X-LIC-LOCATION:" . $timezone . "\r\n"; + + // Get current year for DST transitions + $year = (int)date('Y'); + + // Add STANDARD component (winter time) + $str .= "BEGIN:STANDARD\r\n"; + $str .= "DTSTART:" . $year . "1027T030000\r\n"; + $str .= "TZOFFSETFROM:+0200\r\n"; + $str .= "TZOFFSETTO:+0100\r\n"; + $str .= "RDATE:" . ($year + 1) . "1026T030000\r\n"; + $str .= "TZNAME:CET\r\n"; + $str .= "END:STANDARD\r\n"; + + // Add DAYLIGHT component (summer time) + $str .= "BEGIN:DAYLIGHT\r\n"; + $str .= "DTSTART:" . $year . "0330T020000\r\n"; + $str .= "TZOFFSETFROM:+0100\r\n"; + $str .= "TZOFFSETTO:+0200\r\n"; + $str .= "RDATE:" . ($year + 1) . "0329T020000\r\n"; + $str .= "TZNAME:CEST\r\n"; + $str .= "END:DAYLIGHT\r\n"; + + $str .= "END:VTIMEZONE\r\n"; + return $str; + } + /** * Convert an array returned by IcsMerger::getResult() into valid ics string * @param array $icsMergerResult @@ -207,6 +263,13 @@ public static function getRawText($icsMergerResult) { $str = 'BEGIN:VCALENDAR' . "\r\n"; $str .= IcsMerger::arrayToIcs($icsMergerResult['VCALENDAR']); + + // Add VTIMEZONE block + $timezone = isset($icsMergerResult['VCALENDAR']['X-WR-TIMEZONE']) + ? $icsMergerResult['VCALENDAR']['X-WR-TIMEZONE'] + : 'Europe/Berlin'; + $str .= self::generateVTimeZone($timezone); + foreach ($icsMergerResult['VEVENTS'] as $event) { $str .= 'BEGIN:VEVENT' . "\r\n"; $str .= IcsMerger::arrayToIcs($event);