Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions ics-collector/lib/EventObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -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())
{
Expand All @@ -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)));
}
Expand All @@ -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,
Expand All @@ -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);
Expand Down
79 changes: 71 additions & 8 deletions ics-collector/lib/ics-merger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']) ) {
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down