public function processRecurrences()
{
$events = isset($this->cal['VEVENT']) ? $this->cal['VEVENT'] : array();
if (empty($events)) {
return false;
}
foreach ($events as $anEvent) {
if (isset($anEvent['RRULE']) && $anEvent['RRULE'] != '') {
if (isset($anEvent['DTSTART_array'][0]['TZID']) && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])) {
$initialStartTimeZone = $anEvent['DTSTART_array'][0]['TZID'];
}
$initialStart = new \DateTime($anEvent['DTSTART_array'][1], isset($initialStartTimeZone) ? new \DateTimeZone($initialStartTimeZone) : null);
$initialStartOffset = $initialStart->getOffset();
if (isset($anEvent['DTEND'])) {
if (isset($anEvent['DTEND_array'][0]['TZID']) && $this->isValidTimeZoneId($anEvent['DTSTART_array'][0]['TZID'])) {
$initialEndTimeZone = $anEvent['DTSTART_array'][0]['TZID'];
}
$initialEnd = new \DateTime($anEvent['DTEND_array'][1], isset($initialEndTimeZone) ? new \DateTimeZone($initialEndTimeZone) : null);
$initialEndOffset = $initialEnd->getOffset();
}
// Recurring event, parse RRULE and add appropriate duplicate events
$rrules = array();
$rruleStrings = explode(';', $anEvent['RRULE']);
foreach ($rruleStrings as $s) {
list($k, $v) = explode('=', $s);
$rrules[$k] = $v;
}
// Get frequency
$frequency = $rrules['FREQ'];
// Get Start timestamp
$startTimestamp = $initialStart->getTimeStamp();
if (isset($anEvent['DTEND'])) {
$endTimestamp = $initialEnd->getTimestamp();
} else {
if (isset($anEvent['DURATION'])) {
$duration = end($anEvent['DURATION_array']);
$endTimestamp = date_create($anEvent['DTSTART']);
$endTimestamp->modify($duration->y . ' year');
$endTimestamp->modify($duration->m . ' month');
$endTimestamp->modify($duration->d . ' day');
$endTimestamp->modify($duration->h . ' hour');
$endTimestamp->modify($duration->i . ' minute');
$endTimestamp->modify($duration->s . ' second');
$endTimestamp = date_format($endTimestamp, 'U');
} else {
$endTimestamp = $anEvent['DTSTART_array'][2];
}
}
$eventTimestampOffset = $endTimestamp - $startTimestamp;
// Get Interval
$interval = isset($rrules['INTERVAL']) && $rrules['INTERVAL'] != '' ? $rrules['INTERVAL'] : 1;
$dayNumber = null;
$weekDay = null;
if (in_array($frequency, array('MONTHLY', 'YEARLY')) && isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
// Deal with BYDAY
$dayNumber = intval($rrules['BYDAY']);
if (empty($dayNumber)) {
// Returns 0 when no number defined in BYDAY
if (!isset($rrules['BYSETPOS'])) {
$dayNumber = 1;
// Set first as default
} else {
if (is_numeric($rrules['BYSETPOS'])) {
$dayNumber = $rrules['BYSETPOS'];
}
}
}
$dayNumber = $dayNumber == -1 ? 6 : $dayNumber;
// Override for our custom key (6 => 'last')
$weekDay = substr($rrules['BYDAY'], -2);
}
$untilDefault = date_create('now');
$untilDefault->modify($this->defaultSpan . ' year');
$untilDefault->setTime(23, 59, 59);
// End of the day
if (isset($rrules['UNTIL'])) {
// Get Until
$until = strtotime($rrules['UNTIL']);
} else {
if (isset($rrules['COUNT'])) {
$countOrig = is_numeric($rrules['COUNT']) && $rrules['COUNT'] > 1 ? $rrules['COUNT'] : 0;
$count = $countOrig - 1;
// Remove one to exclude the occurrence that initialises the rule
$count += $count > 0 ? $count * ($interval - 1) : 0;
$countNb = 1;
$offset = "+{$count} " . $this->frequencyConversion[$frequency];
$until = strtotime($offset, $startTimestamp);
if (in_array($frequency, array('MONTHLY', 'YEARLY')) && isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
$dtstart = date_create($anEvent['DTSTART']);
for ($i = 1; $i <= $count; $i++) {
$dtstartClone = clone $dtstart;
$dtstartClone->modify('next ' . $this->frequencyConversion[$frequency]);
$offset = "{$this->dayOrdinals[$dayNumber]} {$this->weekdays[$weekDay]} of " . $dtstartClone->format('F Y H:i:01');
$dtstart->modify($offset);
}
/**
* Jumping X months forwards doesn't mean
* the end date will fall on the same day defined in BYDAY
* Use the largest of these to ensure we are going far enough
* in the future to capture our final end day
*/
$until = max($until, $dtstart->format('U'));
}
unset($offset);
} else {
$until = $untilDefault->getTimestamp();
}
}
if (!isset($anEvent['EXDATE_array'])) {
$anEvent['EXDATE_array'][1] = array();
}
// Decide how often to add events and do so
switch ($frequency) {
case 'DAILY':
// Simply add a new event each interval of days until UNTIL is reached
$offset = "+{$interval} day";
$recurringTimestamp = strtotime($offset, $startTimestamp);
while ($recurringTimestamp <= $until) {
$dayRecurringTimestamp = $recurringTimestamp;
// Adjust timezone from initial event
$recurringTimeZone = \DateTime::createFromFormat('U', $dayRecurringTimestamp);
$timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
$dayRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
// Add event
$anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $dayRecurringTimestamp) . 'Z';
$anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $dayRecurringTimestamp);
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$searchDate = $anEvent['DTSTART'];
$isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
return is_string($val) && strpos($searchDate, $val) === 0;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($dayRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
if (!$isExcluded) {
$events[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
break;
case 'WEEKLY':
// Create offset
$offset = "+{$interval} week";
// Use RRULE['WKST'] setting or a default week start (UK = SU, Europe = MO)
$weeks = array('SA' => array('SA', 'SU', 'MO', 'TU', 'WE', 'TH', 'FR'), 'SU' => array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'), 'MO' => array('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'));
$wkst = isset($rrules['WKST']) && in_array($rrules['WKST'], array('SA', 'SU', 'MO')) ? $rrules['WKST'] : $this->defaultWeekStart;
$aWeek = $weeks[$wkst];
$days = array('SA' => 'Saturday', 'SU' => 'Sunday', 'MO' => 'Monday');
// Build list of days of week to add events
$weekdays = $aWeek;
if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
$bydays = explode(',', $rrules['BYDAY']);
} else {
$findDay = $weekdays[gmdate('w', $startTimestamp)];
$bydays = array($findDay);
}
// Get timestamp of first day of start week
$weekRecurringTimestamp = gmdate('w', $startTimestamp) == 0 ? $startTimestamp : strtotime("last {$days[$wkst]} " . gmdate('H:i:s\\z', $startTimestamp), $startTimestamp);
// Step through weeks
while ($weekRecurringTimestamp <= $until) {
$dayRecurringTimestamp = $weekRecurringTimestamp;
// Adjust timezone from initial event
$dayRecurringTimeZone = \DateTime::createFromFormat('U', $dayRecurringTimestamp, new \DateTimeZone('UTC'));
$timezoneOffset = $initialStart->getTimezone()->getOffset($dayRecurringTimeZone);
$dayRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
foreach ($weekdays as $day) {
// Check if day should be added
if (in_array($day, $bydays) && $dayRecurringTimestamp > $startTimestamp && $dayRecurringTimestamp <= $until) {
// Add event
$anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $dayRecurringTimestamp) . 'Z';
$anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $dayRecurringTimestamp);
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$searchDate = $anEvent['DTSTART'];
$isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
return is_string($val) && strpos($searchDate, $val) === 0;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($dayRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
if (!$isExcluded) {
$events[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
// Move forwards a day
$dayRecurringTimestamp = strtotime('+1 day', $dayRecurringTimestamp);
}
// Move forwards $interval weeks
$weekRecurringTimestamp = strtotime($offset, $weekRecurringTimestamp);
}
break;
case 'MONTHLY':
// Create offset
$offset = "+{$interval} month";
$recurringTimestamp = strtotime($offset, $startTimestamp);
if (isset($rrules['BYMONTHDAY']) && $rrules['BYMONTHDAY'] != '') {
// Deal with BYMONTHDAY
$monthdays = explode(',', $rrules['BYMONTHDAY']);
while ($recurringTimestamp <= $until) {
foreach ($monthdays as $key => $monthday) {
if ($key === 0) {
// Ensure original event conforms to monthday rule
$events[0]['DTSTART'] = gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, strtotime($events[0]['DTSTART'])) . 'Z';
$events[0]['DTEND'] = gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, strtotime($events[0]['DTEND'])) . 'Z';
$events[0]['DTSTART_array'][1] = $events[0]['DTSTART'];
$events[0]['DTSTART_array'][2] = $this->iCalDateToUnixTimestamp($events[0]['DTSTART']);
$events[0]['DTEND_array'][1] = $events[0]['DTEND'];
$events[0]['DTEND_array'][2] = $this->iCalDateToUnixTimestamp($events[0]['DTEND']);
// Ensure recurring timestamp confirms to monthday rule
$monthRecurringTimestamp = $this->iCalDateToUnixTimestamp(gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, $recurringTimestamp) . 'Z');
}
// Adjust timezone from initial event
$recurringTimeZone = \DateTime::createFromFormat('U', $monthRecurringTimestamp);
$timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
$monthRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
// Add event
$anEvent['DTSTART'] = gmdate('Ym' . sprintf('%02d', $monthday) . '\\T' . self::TIME_FORMAT, $monthRecurringTimestamp) . 'Z';
$anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $monthRecurringTimestamp);
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$searchDate = $anEvent['DTSTART'];
$isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
return is_string($val) && strpos($searchDate, $val) === 0;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($monthRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
if (!$isExcluded) {
$events[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
} else {
if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
while ($recurringTimestamp <= $until) {
$monthRecurringTimestamp = $recurringTimestamp;
// Adjust timezone from initial event
$recurringTimeZone = \DateTime::createFromFormat('U', $monthRecurringTimestamp);
$timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
$monthRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
$eventStartDesc = "{$this->dayOrdinals[$dayNumber]} {$this->weekdays[$weekDay]} of " . gmdate('F Y H:i:s', $monthRecurringTimestamp);
$eventStartTimestamp = strtotime($eventStartDesc);
// Prevent 5th day of a month from showing up on the next month
// If BYDAY and the event falls outside the current month, skip the event
$compareCurrentMonth = date('F', $monthRecurringTimestamp);
$compareEventMonth = date('F', $eventStartTimestamp);
if ($compareCurrentMonth != $compareEventMonth) {
$monthRecurringTimestamp = strtotime($offset, $monthRecurringTimestamp);
continue;
}
if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) {
$anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $eventStartTimestamp) . 'Z';
$anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $eventStartTimestamp);
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$searchDate = $anEvent['DTSTART'];
$isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
return is_string($val) && strpos($searchDate, $val) === 0;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($monthRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
if (!$isExcluded) {
$events[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
}
}
break;
case 'YEARLY':
// Create offset
$offset = "+{$interval} year";
$recurringTimestamp = strtotime($offset, $startTimestamp);
// Check if BYDAY rule exists
if (isset($rrules['BYDAY']) && $rrules['BYDAY'] != '') {
while ($recurringTimestamp <= $until) {
$yearRecurringTimestamp = $recurringTimestamp;
// Adjust timezone from initial event
$recurringTimeZone = \DateTime::createFromFormat('U', $yearRecurringTimestamp);
$timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
$yearRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
$eventStartDesc = "{$this->dayOrdinals[$dayNumber]} {$this->weekdays[$weekDay]}" . " of {$this->monthNames[$rrules['BYMONTH']]} " . gmdate('Y H:i:s', $yearRecurringTimestamp);
$eventStartTimestamp = strtotime($eventStartDesc);
if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) {
$anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $eventStartTimestamp) . 'Z';
$anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $eventStartTimestamp);
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$searchDate = $anEvent['DTSTART'];
$isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
return is_string($val) && strpos($searchDate, $val) === 0;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($yearRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
if (!$isExcluded) {
$events[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
} else {
$day = gmdate('d', $startTimestamp);
// Step through years
while ($recurringTimestamp <= $until) {
$yearRecurringTimestamp = $recurringTimestamp;
// Adjust timezone from initial event
$recurringTimeZone = \DateTime::createFromFormat('U', $yearRecurringTimestamp);
$timezoneOffset = $initialStart->getTimezone()->getOffset($recurringTimeZone);
$yearRecurringTimestamp += $timezoneOffset != $initialStartOffset ? $initialStartOffset - $timezoneOffset : 0;
// Add specific month dates
if (isset($rrules['BYMONTH']) && $rrules['BYMONTH'] != '') {
$eventStartDesc = "{$day} {$this->monthNames[$rrules['BYMONTH']]} " . gmdate('Y H:i:s', $yearRecurringTimestamp);
} else {
$eventStartDesc = $day . gmdate('F Y H:i:s', $yearRecurringTimestamp);
}
$eventStartTimestamp = strtotime($eventStartDesc);
if ($eventStartTimestamp > $startTimestamp && $eventStartTimestamp < $until) {
$anEvent['DTSTART'] = gmdate(self::DATE_TIME_FORMAT, $eventStartTimestamp) . 'Z';
$anEvent['DTSTART_array'] = array(array(), $anEvent['DTSTART'], $eventStartTimestamp);
$anEvent['DTEND_array'] = $anEvent['DTSTART_array'];
$anEvent['DTEND_array'][2] += $eventTimestampOffset;
$anEvent['DTEND'] = gmdate(self::DATE_TIME_FORMAT, $anEvent['DTEND_array'][2]) . 'Z';
$anEvent['DTEND_array'][1] = $anEvent['DTEND'];
$searchDate = $anEvent['DTSTART'];
$isExcluded = array_filter($anEvent['EXDATE_array'][1], function ($val) use($searchDate) {
return is_string($val) && strpos($searchDate, $val) === 0;
});
if (isset($this->alteredRecurrenceInstances[$anEvent['UID']]) && in_array($yearRecurringTimestamp, $this->alteredRecurrenceInstances[$anEvent['UID']])) {
$isExcluded = true;
}
if (!$isExcluded) {
$events[] = $anEvent;
$this->eventCount++;
// If RRULE[COUNT] is reached then break
if (isset($rrules['COUNT'])) {
$countNb++;
if ($countNb >= $countOrig) {
break 2;
}
}
}
}
// Move forwards
$recurringTimestamp = strtotime($offset, $recurringTimestamp);
}
}
break;
$events = isset($countOrig) && sizeof($events) > $countOrig ? array_slice($events, 0, $countOrig) : $events;
// Ensure we abide by COUNT if defined
}
}
}
$this->cal['VEVENT'] = $events;
}