diff --git a/calendar/inc/class.calendar_bo.inc.php b/calendar/inc/class.calendar_bo.inc.php index ecbd069d5c..3d640e27f6 100644 --- a/calendar/inc/class.calendar_bo.inc.php +++ b/calendar/inc/class.calendar_bo.inc.php @@ -128,7 +128,8 @@ class calendar_bo MCAL_RECUR_WEEKLY => 'Weekly', MCAL_RECUR_MONTHLY_WDAY => 'Monthly (by day)', MCAL_RECUR_MONTHLY_MDAY => 'Monthly (by date)', - MCAL_RECUR_YEARLY => 'Yearly' + MCAL_RECUR_YEARLY => 'Yearly', + MCAL_RECUR_RDATE/*calendar_rrule::PERIOD*/ => 'Explicit dates', ); /** * @var array recur_days translates MCAL recur-days to verbose labels @@ -946,7 +947,7 @@ class calendar_bo foreach($events as $event) { // PERIOD - $is_exception = $event['recur_type'] != calendar_rrule::PERIOD && in_array(Api\DateTime::to($event['start'], true), $exceptions); + $is_exception = in_array(Api\DateTime::to($event['start'], true), $exceptions); $start = $this->date2ts($event['start'],true); if ($event['whole_day']) { @@ -1024,10 +1025,11 @@ class calendar_bo $event[$ts] = $this->date2usertime((int)$event[$ts],$date_format); } } - // same with the recur exceptions - if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + // same with the recur exceptions and rdates + foreach(['recur_exception', 'recur_rdates'] as $name) { - foreach($event['recur_exception'] as &$date) + if (is_array($event[$name] ?? null)) continue; + foreach($event[$name] as &$date) { if ($event['whole_day'] && $date_format != 'server') { @@ -1139,7 +1141,6 @@ class calendar_bo * @param mixed $start start-date * @param mixed $end end-date * @param array $events where the repetions get inserted - * @param array $recur_exceptions with date (in Ymd) as key (and True as values), seems not to be used anymore */ function insert_all_recurrences($event,$_start,$end,&$events) { @@ -1165,20 +1166,15 @@ 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 - // (Period needs them though) - if($event['recur_type'] != calendar_rrule::PERIOD) - { - unset($event['recur_exception']); - } + 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']); - } + unset($event['recur_rdates']); + $event['recur_type'] = MCAL_RECUR_NONE; + foreach($rrule as $time) { // $time is in timezone of event, convert it to usertime used here @@ -1194,7 +1190,7 @@ class calendar_bo if (($ts = $this->date2ts($time)) < $start-$event_length) { //echo "

".$time." --> ignored as $ts < $start-$event_length

\n"; - continue; // to early or original event (returned by interator too) + continue; // to early or original event (returned by iterator too) } $ts_end = $ts + $event_length; @@ -1230,7 +1226,7 @@ class calendar_bo } /** - * Adds one repetion of $event for $date_ymd to the $events array, after adjusting its start- and end-time + * Adds one repetition of $event for $date_ymd to the $events array, after adjusting its start- and end-time * * @param array $events array in which the event gets inserted * @param array $event event to insert, it has start- and end-date of the first recurrence, not of $date_ymd diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 35e305e7f5..f5b20cf33b 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -1404,7 +1404,7 @@ class calendar_boupdate extends calendar_bo $this->check_reset_statuses($event, $old_event); // set recur-enddate/range-end to real end-date of last recurrence - if (!empty($event['recur_type']) && $event['recur_enddate'] && $event['start']) + if (!empty($event['recur_type']) && (!empty($event['recur_enddate']) || $event['recur_type'] == calendar_rrule::PERIOD) && $event['start']) { $event['recur_enddate'] = new Api\DateTime($event['recur_enddate'], calendar_timezones::DateTimeZone($event['tzid'])); $event['recur_enddate']->setTime(23,59,59); @@ -1485,10 +1485,11 @@ class calendar_boupdate extends calendar_bo { $event['tz_id'] = calendar_timezones::tz2id($event['tzid'] = Api\DateTime::$user_timezone->getName()); } - // same with the recur exceptions - if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + // same with the recur exceptions and rdates + foreach(['recur_exception', 'recur_rdates'] as $name) { - foreach($event['recur_exception'] as &$date) + if (!is_array($event[$name] ?? null)) continue; + foreach($event[$name] as &$date) { if ($event['whole_day']) { @@ -2400,9 +2401,9 @@ class calendar_boupdate extends calendar_bo * * @param array $event the vCalendar data we try to find * @param string filter='exact' exact -> find the matching entry - * check -> check (consitency) for identical matches + * check -> check (consistency) for identical matches * relax -> be more tolerant - * master -> try to find a releated series master + * master -> try to find a related series master * @return array calendar_ids of matching entries */ function find_event($event, $filter='exact') @@ -3120,12 +3121,12 @@ class calendar_boupdate extends calendar_bo // we convert here from server-time to timestamps in user-time! if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2usertime($event[$ts]) : 0; } - // same with the recur exceptions - if (isset($event['recur_exception']) && is_array($event['recur_exception'])) + // same with the recur exceptions and rdates + foreach(['recur_exception', 'recur_rdates'] as $name) { - foreach($event['recur_exception'] as $n => $date) + foreach($event[$name] ?? [] as $n => $date) { - $event['recur_exception'][$n] = $this->date2usertime($date); + $event[$name][$n] = $this->date2usertime($date); } } // same with the alarms @@ -3137,6 +3138,7 @@ class calendar_boupdate extends calendar_bo } } } + /** * Delete events that are more than $age years old * diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index e4f45b1018..4404926c87 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -2760,10 +2760,10 @@ class calendar_ical extends calendar_boupdate if($attributes['params']['VALUE'] == 'PERIOD') { $vcardData['recur_type'] = calendar_rrule::PERIOD; - $vcardData['recur_exception'] = []; + $vcardData['recur_rdates'] = []; foreach($attributes['values'] as $date) { - $vcardData['recur_exception'][] = mktime( + $vcardData['recur_rdates'][] = mktime( $hour, $minutes, $seconds, @@ -3180,7 +3180,7 @@ class calendar_ical extends calendar_boupdate $event['priority'] = 2; // default $event['alarm'] = $alarms; - // now that we know what the vard provides, + // now that we know what the ical provides, // we merge that data with the information we have about the device while (($fieldName = array_shift($supportedFields))) { @@ -3191,6 +3191,7 @@ class calendar_ical extends calendar_boupdate case 'recur_data': case 'recur_exception': case 'recur_count': + case 'recur_rdates': case 'whole_day': // not handled here break; @@ -3200,7 +3201,7 @@ class calendar_ical extends calendar_boupdate if ($event['recur_type'] != MCAL_RECUR_NONE) { $event['reference'] = 0; - foreach (array('recur_interval','recur_enddate','recur_data','recur_exception','recur_count') as $r) + foreach (array('recur_interval','recur_enddate','recur_data','recur_exception','recur_count','recur_rdates') as $r) { if (isset($vcardData[$r])) { diff --git a/calendar/inc/class.calendar_rrule.inc.php b/calendar/inc/class.calendar_rrule.inc.php index df282f846f..469679859b 100644 --- a/calendar/inc/class.calendar_rrule.inc.php +++ b/calendar/inc/class.calendar_rrule.inc.php @@ -45,29 +45,28 @@ class calendar_rrule implements Iterator */ const WEEKLY = 2; /** - * Monthly recurrance iCal: monthly_bymonthday + * Monthly recurrence iCal: monthly_bymonthday */ const MONTHLY_MDAY = 3; /** - * Monthly recurrance iCal: BYDAY (by weekday, eg. 1st Friday of month) + * Monthly recurrence iCal: BYDAY (by weekday, eg. 1st Friday of month) */ const MONTHLY_WDAY = 4; /** - * Yearly recurrance + * Yearly recurrence */ const YEARLY = 5; /** - * Hourly recurrance + * Hourly recurrence */ const HOURLY = 8; /** - * Minutely recurrance + * Minutely recurrence */ const MINUTELY = 7; /** - * By date or period - * (a list of dates) + * RDATE: date or period (a list of dates, instead of a RRULE) */ const PERIOD = 9; @@ -226,9 +225,9 @@ class calendar_rrule implements Iterator public $time; /** - * Current "position" / time + * Current "position" / time or null, if invalid (out of explicit RDATEs) * - * @var Api\DateTime + * @var Api\DateTime|null */ public $current; @@ -257,9 +256,10 @@ class calendar_rrule implements Iterator * @param int $interval =1 1, 2, ... * @param DateTime $enddate =null enddate or null for no enddate (in which case we user '+5 year' on $time) * @param int $weekdays =0 self::SUNDAY=1|self::MONDAY=2|...|self::SATURDAY=64 - * @param array $exceptions =null DateTime objects with exceptions + * @param DateTime[] $exceptions =null DateTime objects with exceptions + * @param DateTime[] $rdates =null DateTime objects with rdates (for type self::PERIOD) */ - public function __construct(DateTime $time,$type,$interval=1,DateTime $enddate=null,$weekdays=0,array $exceptions=null) + public function __construct(DateTime $time,$type,$interval=1,DateTime $enddate=null,$weekdays=0,array $exceptions=null,array $rdates=null) { switch($GLOBALS['egw_info']['user']['preferences']['calendar']['weekdaystarts']) { @@ -319,16 +319,15 @@ class calendar_rrule implements Iterator $this->enddate = $enddate; if($type == self::PERIOD) { - foreach($exceptions as $exception) + foreach($rdates as $rdate) { - $exception->setTimezone($this->time->getTimezone()); - $this->period[] = $exception; + $rdate->setTimezone($this->time->getTimezone()); + $this->period[] = $rdate; } $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) @@ -413,11 +412,11 @@ class calendar_rrule implements Iterator /** * Return the current element * - * @return Api\DateTime + * @return ?Api\DateTime */ - public function current(): Api\DateTime + public function current(): ?Api\DateTime { - return clone $this->current; + return $this->current ? clone $this->current : null; } /** @@ -427,7 +426,7 @@ class calendar_rrule implements Iterator */ public function key(): int { - return (int)$this->current->format('Ymd'); + return $this->current ? (int)$this->current->format('Ymd') : 0; } /** @@ -498,14 +497,15 @@ class calendar_rrule implements Iterator $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)) + if (($next = next($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); + } + else + { + $this->current = null; } - $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: @@ -522,7 +522,7 @@ class calendar_rrule implements Iterator { $this->next_no_exception(); } - while($this->exceptions && in_array($this->current->format('Ymd'),$this->exceptions)); + while($this->current && $this->exceptions && in_array($this->current->format('Ymd'),$this->exceptions)); } /** @@ -595,6 +595,10 @@ class calendar_rrule implements Iterator */ public function rewind(): void { + if ($this->type == self::PERIOD) + { + reset($this->period); + } $this->current = clone $this->time; while ($this->valid() && $this->exceptions && @@ -612,6 +616,10 @@ class calendar_rrule implements Iterator */ public function validDate(bool $use_just_date=null): bool { + if (!$this->current) + { + return false; + } if ($use_just_date) { return $this->current->format('Ymd') <= $this->enddate_ymd; @@ -626,7 +634,7 @@ class calendar_rrule implements Iterator */ public function valid(): bool { - return $this->current->format('ts') < $this->enddate_ts; + return $this->current && $this->current->format('ts') < $this->enddate_ts; } /** @@ -853,10 +861,17 @@ class calendar_rrule implements Iterator { foreach($event['recur_exception'] as $exception) { - $exceptions[] = is_a($exception,'DateTime') ? $exception : new Api\DateTime($exception,$timestamp_tz); + $exceptions[] = is_a($exception,'DateTime') ? $exception : new Api\DateTime($exception, $timestamp_tz); } } - return new calendar_rrule($time,$event['recur_type'],$event['recur_interval'],$enddate??null,$event['recur_data'],$exceptions??null); + if (is_array($event['recur_rdates'])) + { + foreach($event['recur_rdates'] as $rdate) + { + $rdates[] = is_a($rdate,'DateTime') ? $rdate : new Api\DateTime($rdate, $timestamp_tz); + } + } + return new calendar_rrule($time,$event['recur_type'],$event['recur_interval'],$enddate??null,$event['recur_data'],$exceptions??null,$rdates??null); } /** @@ -955,6 +970,7 @@ class calendar_rrule implements Iterator 'recur_enddate' => $this->enddate ? $this->enddate->format('ts') : null, 'recur_data' => $this->weekdays, 'recur_exception' => $this->exceptions, + 'recur_rdates' => $this->period, ); } diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php index d72cb8ed51..818667464f 100644 --- a/calendar/inc/class.calendar_so.inc.php +++ b/calendar/inc/class.calendar_so.inc.php @@ -43,6 +43,7 @@ if(!extension_loaded('mcal')) define('MCAL_M_WEEKEND',65); define('MCAL_M_ALLDAYS',127); } +define('MCAL_RECUR_RDATE',9); define('REJECTED',0); define('NO_RESPONSE',1); @@ -311,7 +312,7 @@ class calendar_so * All times (start, end and modified) are returned as timesstamps in servertime! * * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid - * @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initital one + * @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initial one * @param boolean $read_recurrence =false true: read the exception, not the series master (only for recur_date && $ids=''!) * @return array|boolean array with cal_id => event array pairs or false if entry not found */ @@ -426,12 +427,21 @@ class calendar_so } if (!(int)$recur_date && !empty($event['recur_type'])) { - foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array( + foreach($this->db->select($this->dates_table, 'cal_id,cal_start,recur_exception', [ 'cal_id' => $ids, + ]+($event['recur_type'] == MCAL_RECUR_RDATE ? [] : [ 'recur_exception' => true, - ), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row) + ]), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row) { - $events[$row['cal_id']]['recur_exception'][] = $row['cal_start']; + if ($row['recur_exception']) + { + $events[$row['cal_id']]['recur_exception'][] = $row['cal_start']; + } + // rdates are both, exceptions and regular dates! + if ($event['recur_type'] == MCAL_RECUR_RDATE) + { + $events[$row['cal_id']]['recur_rdates'][] = $row['cal_start']; + } } break; // as above select read all exceptions (and I dont think too short uid problem still exists) } @@ -1496,7 +1506,7 @@ ORDER BY cal_user_type, cal_usre_id if ($cal_id) { - // query old recurrance information, before updating main table, where recur_endate is now stored + // query old recurrence information, before updating main table, where recur_endate is now stored if (!empty($event['recur_type'])) { $old_repeats = $this->db->select($this->repeats_table, "$this->repeats_table.*,range_end AS recur_enddate", @@ -1610,7 +1620,7 @@ ORDER BY cal_user_type, cal_usre_id } $event['recur_exception'] = is_array($event['recur_exception']) ? $event['recur_exception'] : array(); - if (!empty($event['recur_exception'])) + if (count($event['recur_exception']) > 1) { sort($event['recur_exception']); } @@ -1707,7 +1717,7 @@ ORDER BY cal_user_type, cal_usre_id // truncate recurrences by given exceptions if (count($event['recur_exception'])) { - // added and existing exceptions: delete the execeptions from the user table, it could be the first time + // added and existing exceptions: delete the exceptions from the user table, it could be the first time $this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date' => $event['recur_exception']),__LINE__,__FILE__,'calendar'); // update recur_exception flag based on current exceptions $this->db->update($this->dates_table, 'recur_exception='.$this->db->expression($this->dates_table,array( diff --git a/calendar/inc/class.calendar_timezones.inc.php b/calendar/inc/class.calendar_timezones.inc.php index 6f298b7147..852f31b0ca 100644 --- a/calendar/inc/class.calendar_timezones.inc.php +++ b/calendar/inc/class.calendar_timezones.inc.php @@ -113,6 +113,11 @@ class calendar_timezones self::$tz_cache[$id] = Api\Db::strip_array_keys($data,'tz_'); } } + // check for a Windows timezone without "Standard Time" postfix + if (!isset($id) && strpos($tzid, '/') === false) + { + $id = self::tz2id($tzid.' Standard Time'); + } if (isset($id) && $what != 'id') { return self::id2tz($id,$what);