From cc98141e865c32e413822e9b9e36dd7fbf7a716e Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 13 Nov 2023 17:21:15 -0700 Subject: [PATCH] Calendar: iCal can import events that use RDATE:VALUE=PERIOD --- calendar/inc/class.calendar_bo.inc.php | 13 ++++++- calendar/inc/class.calendar_ical.inc.php | 21 ++++++++++ calendar/inc/class.calendar_rrule.inc.php | 47 ++++++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index e7fb11c1e5..ecbd069d5c 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -945,7 +945,8 @@ class calendar_bo } foreach($events as $event) { - $is_exception = in_array(Api\DateTime::to($event['start'], true), $exceptions); + // PERIOD + $is_exception = $event['recur_type'] != calendar_rrule::PERIOD && in_array(Api\DateTime::to($event['start'], true), $exceptions); $start = $this->date2ts($event['start'],true); if ($event['whole_day']) { @@ -1164,12 +1165,20 @@ class calendar_bo new Api\DateTime($event['recur_enddate'], calendar_timezones::DateTimeZone($event['tzid'])); // unset exceptions, as we need to add them as recurrence too, but marked as exception - unset($event['recur_exception']); + // (Period needs them though) + if($event['recur_type'] != calendar_rrule::PERIOD) + { + unset($event['recur_exception']); + } // loop over all recurrences and insert them, if they are after $start $rrule = calendar_rrule::event2rrule($event, !$event['whole_day'], // true = we operate in usertime, like the rest of calendar_bo // For whole day events, just stay in server time $event['whole_day'] ? Api\DateTime::$server_timezone->getName() : Api\DateTime::$user_timezone->getName() ); + if($event['recur_type'] == calendar_rrule::PERIOD) + { + unset($event['recur_exception']); + } foreach($rrule as $time) { // $time is in timezone of event, convert it to usertime used here diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index 87f3ad03f7..e4f45b1018 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -2753,6 +2753,27 @@ class calendar_ical extends calendar_boupdate $vcardData += calendar_rrule::parseRrule($attributes['value'], false, $vcardData); if (!empty($vcardData['recur_enddate'])) self::check_fix_endate ($vcardData); break; + case 'RDATE': + $hour = date('H', $vcardData['start']); + $minutes = date('i', $vcardData['start']); + $seconds = date('s', $vcardData['start']); + if($attributes['params']['VALUE'] == 'PERIOD') + { + $vcardData['recur_type'] = calendar_rrule::PERIOD; + $vcardData['recur_exception'] = []; + foreach($attributes['values'] as $date) + { + $vcardData['recur_exception'][] = mktime( + $hour, + $minutes, + $seconds, + $date['month'], + $date['mday'], + $date['year'] + ); + } + } + break; case 'EXDATE': // current Horde_Icalendar returns dates, no timestamps if ($attributes['values']) { diff --git a/calendar/inc/class.calendar_rrule.inc.php b/calendar/inc/class.calendar_rrule.inc.php index 028fccf087..df282f846f 100644 --- a/calendar/inc/class.calendar_rrule.inc.php +++ b/calendar/inc/class.calendar_rrule.inc.php @@ -65,6 +65,12 @@ class calendar_rrule implements Iterator */ const MINUTELY = 7; + /** + * By date or period + * (a list of dates) + */ + const PERIOD = 9; + /** * Translate recure types to labels * @@ -77,6 +83,7 @@ class calendar_rrule implements Iterator self::MONTHLY_WDAY => 'Monthly (by day)', self::MONTHLY_MDAY => 'Monthly (by date)', self::YEARLY => 'Yearly', + self::PERIOD => 'By date or period' ); /** @@ -90,6 +97,7 @@ class calendar_rrule implements Iterator self::YEARLY => 'YEARLY', self::HOURLY => 'HOURLY', self::MINUTELY => 'MINUTELY', + self::PERIOD => 'PERIOD' ); /** @@ -135,6 +143,12 @@ class calendar_rrule implements Iterator */ public $monthly_bymonthday; + /** + * Period list + * @var + */ + public $period = []; + /** * Enddate of recurring event or null, if not ending * @@ -261,7 +275,8 @@ class calendar_rrule implements Iterator $this->time = $time instanceof Api\DateTime ? $time : new Api\DateTime($time); - if (!in_array($type,array(self::NONE, self::DAILY, self::WEEKLY, self::MONTHLY_MDAY, self::MONTHLY_WDAY, self::YEARLY, self::HOURLY, self::MINUTELY))) + if(!in_array($type, array(self::NONE, self::DAILY, self::WEEKLY, self::MONTHLY_MDAY, self::MONTHLY_WDAY, + self::YEARLY, self::HOURLY, self::MINUTELY, self::PERIOD))) { throw new Api\Exception\WrongParameter(__METHOD__."($time,$type,$interval,$enddate,$weekdays,...) type $type is NOT valid!"); } @@ -302,6 +317,19 @@ class calendar_rrule implements Iterator $this->interval = (int)$interval; $this->enddate = $enddate; + if($type == self::PERIOD) + { + foreach($exceptions as $exception) + { + $exception->setTimezone($this->time->getTimezone()); + $this->period[] = $exception; + } + $enddate = clone(count($this->period) ? end($this->period) : $this->time); + // Make sure to include the last date as valid + $enddate->modify('+1 second'); + reset($this->period); + unset($exceptions); + } // no recurrence --> current date is enddate if ($type == self::NONE) { @@ -469,6 +497,16 @@ class calendar_rrule implements Iterator case self::MINUTELY: $this->current->modify($this->interval.' minute'); break; + case self::PERIOD: + $index = array_search($this->current, $this->period); + $next = $this->enddate ?? new Api\DateTime(); + if($index !== false && $index + 1 < count($this->period)) + { + $next = $this->period[$index + 1]; + } + $this->current->setDate($next->format('Y'), $next->format('m'), $next->format('d')); + $this->current->setTime($next->format('H'), $next->format('i'), $next->format('s'), 0); + break; default: throw new Api\Exception\AssertionFailed(__METHOD__."() invalid type #$this->type !"); @@ -737,6 +775,13 @@ class calendar_rrule implements Iterator $rrule['BYDAY'] = $this->monthly_byday_num . strtoupper(substr($this->time->format('l'),0,2)); break; + case self::PERIOD: + $period = []; + foreach($this->period as $date) + { + $period[] = $date->format("Ymd\THms\Z"); + } + $rrule['PERIOD'] = implode(',', $period); } if ($this->interval > 1) {