* Calendar: fix generating/importing exceptions on recurring events using explicit RDATEs instead of a RRULE

also correctly recognize Windows timezone names without "Standard Time" postfix like "Romance" instead of "Romance Standard Time" for "Europe/Paris"
This commit is contained in:
ralf 2024-06-04 15:30:54 +02:00
parent cc98141e86
commit 2bfcc20856
6 changed files with 97 additions and 67 deletions

View File

@ -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']);
}
// 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 "<p>".$time." --> ignored as $ts < $start-$event_length</p>\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

View File

@ -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
*

View File

@ -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]))
{

View File

@ -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;
}
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,
);
}

View File

@ -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='<uid>'!)
* @return array|boolean array with cal_id => event array pairs or false if entry not found
*/
@ -426,13 +427,22 @@ 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)
{
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)
}
// make sure we fetch only real exceptions (deleted occurrences of a series should not show up)
@ -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(

View File

@ -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);