mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-11-15 12:37:14 +01:00
487 lines
14 KiB
PHP
487 lines
14 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Sabre\VObject\Recur;
|
||
|
|
||
|
use DateTimeImmutable;
|
||
|
use DateTimeInterface;
|
||
|
use DateTimeZone;
|
||
|
use InvalidArgumentException;
|
||
|
use Sabre\VObject\Component;
|
||
|
use Sabre\VObject\Component\VEvent;
|
||
|
use Sabre\VObject\Settings;
|
||
|
|
||
|
/**
|
||
|
* This class is used to determine new for a recurring event, when the next
|
||
|
* events occur.
|
||
|
*
|
||
|
* This iterator may loop infinitely in the future, therefore it is important
|
||
|
* that if you use this class, you set hard limits for the amount of iterations
|
||
|
* you want to handle.
|
||
|
*
|
||
|
* Note that currently there is not full support for the entire iCalendar
|
||
|
* specification, as it's very complex and contains a lot of permutations
|
||
|
* that's not yet used very often in software.
|
||
|
*
|
||
|
* For the focus has been on features as they actually appear in Calendaring
|
||
|
* software, but this may well get expanded as needed / on demand
|
||
|
*
|
||
|
* The following RRULE properties are supported
|
||
|
* * UNTIL
|
||
|
* * INTERVAL
|
||
|
* * COUNT
|
||
|
* * FREQ=DAILY
|
||
|
* * BYDAY
|
||
|
* * BYHOUR
|
||
|
* * BYMONTH
|
||
|
* * FREQ=WEEKLY
|
||
|
* * BYDAY
|
||
|
* * BYHOUR
|
||
|
* * WKST
|
||
|
* * FREQ=MONTHLY
|
||
|
* * BYMONTHDAY
|
||
|
* * BYDAY
|
||
|
* * BYSETPOS
|
||
|
* * FREQ=YEARLY
|
||
|
* * BYMONTH
|
||
|
* * BYYEARDAY
|
||
|
* * BYWEEKNO
|
||
|
* * BYMONTHDAY (only if BYMONTH is also set)
|
||
|
* * BYDAY (only if BYMONTH is also set)
|
||
|
*
|
||
|
* Anything beyond this is 'undefined', which means that it may get ignored, or
|
||
|
* you may get unexpected results. The effect is that in some applications the
|
||
|
* specified recurrence may look incorrect, or is missing.
|
||
|
*
|
||
|
* The recurrence iterator also does not yet support THISANDFUTURE.
|
||
|
*
|
||
|
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||
|
* @author Evert Pot (http://evertpot.com/)
|
||
|
* @license http://sabre.io/license/ Modified BSD License
|
||
|
*/
|
||
|
class EventIterator implements \Iterator
|
||
|
{
|
||
|
/**
|
||
|
* Reference timeZone for floating dates and times.
|
||
|
*
|
||
|
* @var DateTimeZone
|
||
|
*/
|
||
|
protected $timeZone;
|
||
|
|
||
|
/**
|
||
|
* True if we're iterating an all-day event.
|
||
|
*
|
||
|
* @var bool
|
||
|
*/
|
||
|
protected $allDay = false;
|
||
|
|
||
|
/**
|
||
|
* Creates the iterator.
|
||
|
*
|
||
|
* There's three ways to set up the iterator.
|
||
|
*
|
||
|
* 1. You can pass a VCALENDAR component and a UID.
|
||
|
* 2. You can pass an array of VEVENTs (all UIDS should match).
|
||
|
* 3. You can pass a single VEVENT component.
|
||
|
*
|
||
|
* Only the second method is recomended. The other 1 and 3 will be removed
|
||
|
* at some point in the future.
|
||
|
*
|
||
|
* The $uid parameter is only required for the first method.
|
||
|
*
|
||
|
* @param Component|array $input
|
||
|
* @param string|null $uid
|
||
|
* @param DateTimeZone $timeZone reference timezone for floating dates and
|
||
|
* times
|
||
|
*/
|
||
|
public function __construct($input, $uid = null, DateTimeZone $timeZone = null)
|
||
|
{
|
||
|
if (is_null($timeZone)) {
|
||
|
$timeZone = new DateTimeZone('UTC');
|
||
|
}
|
||
|
$this->timeZone = $timeZone;
|
||
|
|
||
|
if (is_array($input)) {
|
||
|
$events = $input;
|
||
|
} elseif ($input instanceof VEvent) {
|
||
|
// Single instance mode.
|
||
|
$events = [$input];
|
||
|
} else {
|
||
|
// Calendar + UID mode.
|
||
|
$uid = (string) $uid;
|
||
|
if (!$uid) {
|
||
|
throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
|
||
|
}
|
||
|
if (!isset($input->VEVENT)) {
|
||
|
throw new InvalidArgumentException('No events found in this calendar');
|
||
|
}
|
||
|
$events = $input->getByUID($uid);
|
||
|
}
|
||
|
|
||
|
foreach ($events as $vevent) {
|
||
|
if (!isset($vevent->{'RECURRENCE-ID'})) {
|
||
|
$this->masterEvent = $vevent;
|
||
|
} else {
|
||
|
$this->exceptions[
|
||
|
$vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
|
||
|
] = true;
|
||
|
$this->overriddenEvents[] = $vevent;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!$this->masterEvent) {
|
||
|
// No base event was found. CalDAV does allow cases where only
|
||
|
// overridden instances are stored.
|
||
|
//
|
||
|
// In this particular case, we're just going to grab the first
|
||
|
// event and use that instead. This may not always give the
|
||
|
// desired result.
|
||
|
if (!count($this->overriddenEvents)) {
|
||
|
throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
|
||
|
}
|
||
|
$this->masterEvent = array_shift($this->overriddenEvents);
|
||
|
}
|
||
|
|
||
|
$this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone);
|
||
|
$this->allDay = !$this->masterEvent->DTSTART->hasTime();
|
||
|
|
||
|
if (isset($this->masterEvent->EXDATE)) {
|
||
|
foreach ($this->masterEvent->EXDATE as $exDate) {
|
||
|
foreach ($exDate->getDateTimes($this->timeZone) as $dt) {
|
||
|
$this->exceptions[$dt->getTimeStamp()] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (isset($this->masterEvent->DTEND)) {
|
||
|
$this->eventDuration =
|
||
|
$this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() -
|
||
|
$this->startDate->getTimeStamp();
|
||
|
} elseif (isset($this->masterEvent->DURATION)) {
|
||
|
$duration = $this->masterEvent->DURATION->getDateInterval();
|
||
|
$end = clone $this->startDate;
|
||
|
$end = $end->add($duration);
|
||
|
$this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp();
|
||
|
} elseif ($this->allDay) {
|
||
|
$this->eventDuration = 3600 * 24;
|
||
|
} else {
|
||
|
$this->eventDuration = 0;
|
||
|
}
|
||
|
|
||
|
if (isset($this->masterEvent->RDATE)) {
|
||
|
$this->recurIterator = new RDateIterator(
|
||
|
$this->masterEvent->RDATE->getParts(),
|
||
|
$this->startDate
|
||
|
);
|
||
|
} elseif (isset($this->masterEvent->RRULE)) {
|
||
|
$this->recurIterator = new RRuleIterator(
|
||
|
$this->masterEvent->RRULE->getParts(),
|
||
|
$this->startDate
|
||
|
);
|
||
|
} else {
|
||
|
$this->recurIterator = new RRuleIterator(
|
||
|
[
|
||
|
'FREQ' => 'DAILY',
|
||
|
'COUNT' => 1,
|
||
|
],
|
||
|
$this->startDate
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->rewind();
|
||
|
if (!$this->valid()) {
|
||
|
throw new NoInstancesException('This recurrence rule does not generate any valid instances');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the date for the current position of the iterator.
|
||
|
*
|
||
|
* @return DateTimeImmutable
|
||
|
*/
|
||
|
public function current()
|
||
|
{
|
||
|
if ($this->currentDate) {
|
||
|
return clone $this->currentDate;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method returns the start date for the current iteration of the
|
||
|
* event.
|
||
|
*
|
||
|
* @return DateTimeImmutable
|
||
|
*/
|
||
|
public function getDtStart()
|
||
|
{
|
||
|
if ($this->currentDate) {
|
||
|
return clone $this->currentDate;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This method returns the end date for the current iteration of the
|
||
|
* event.
|
||
|
*
|
||
|
* @return DateTimeImmutable
|
||
|
*/
|
||
|
public function getDtEnd()
|
||
|
{
|
||
|
if (!$this->valid()) {
|
||
|
return;
|
||
|
}
|
||
|
$end = clone $this->currentDate;
|
||
|
|
||
|
return $end->modify('+'.$this->eventDuration.' seconds');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a VEVENT for the current iterations of the event.
|
||
|
*
|
||
|
* This VEVENT will have a recurrence id, and its DTSTART and DTEND
|
||
|
* altered.
|
||
|
*
|
||
|
* @return VEvent
|
||
|
*/
|
||
|
public function getEventObject()
|
||
|
{
|
||
|
if ($this->currentOverriddenEvent) {
|
||
|
return $this->currentOverriddenEvent;
|
||
|
}
|
||
|
|
||
|
$event = clone $this->masterEvent;
|
||
|
|
||
|
// Ignoring the following block, because PHPUnit's code coverage
|
||
|
// ignores most of these lines, and this messes with our stats.
|
||
|
//
|
||
|
// @codeCoverageIgnoreStart
|
||
|
unset(
|
||
|
$event->RRULE,
|
||
|
$event->EXDATE,
|
||
|
$event->RDATE,
|
||
|
$event->EXRULE,
|
||
|
$event->{'RECURRENCE-ID'}
|
||
|
);
|
||
|
// @codeCoverageIgnoreEnd
|
||
|
|
||
|
$event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating());
|
||
|
if (isset($event->DTEND)) {
|
||
|
$event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating());
|
||
|
}
|
||
|
$recurid = clone $event->DTSTART;
|
||
|
$recurid->name = 'RECURRENCE-ID';
|
||
|
$event->add($recurid);
|
||
|
|
||
|
return $event;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the current position of the iterator.
|
||
|
*
|
||
|
* This is for us simply a 0-based index.
|
||
|
*
|
||
|
* @return int
|
||
|
*/
|
||
|
public function key()
|
||
|
{
|
||
|
// The counter is always 1 ahead.
|
||
|
return $this->counter - 1;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This is called after next, to see if the iterator is still at a valid
|
||
|
* position, or if it's at the end.
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function valid()
|
||
|
{
|
||
|
if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) {
|
||
|
throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences);
|
||
|
}
|
||
|
|
||
|
return (bool) $this->currentDate;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the iterator back to the starting point.
|
||
|
*/
|
||
|
public function rewind()
|
||
|
{
|
||
|
$this->recurIterator->rewind();
|
||
|
// re-creating overridden event index.
|
||
|
$index = [];
|
||
|
foreach ($this->overriddenEvents as $key => $event) {
|
||
|
$stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
|
||
|
$index[$stamp][] = $key;
|
||
|
}
|
||
|
krsort($index);
|
||
|
$this->counter = 0;
|
||
|
$this->overriddenEventsIndex = $index;
|
||
|
$this->currentOverriddenEvent = null;
|
||
|
|
||
|
$this->nextDate = null;
|
||
|
$this->currentDate = clone $this->startDate;
|
||
|
|
||
|
$this->next();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Advances the iterator with one step.
|
||
|
*/
|
||
|
public function next()
|
||
|
{
|
||
|
$this->currentOverriddenEvent = null;
|
||
|
++$this->counter;
|
||
|
if ($this->nextDate) {
|
||
|
// We had a stored value.
|
||
|
$nextDate = $this->nextDate;
|
||
|
$this->nextDate = null;
|
||
|
} else {
|
||
|
// We need to ask rruleparser for the next date.
|
||
|
// We need to do this until we find a date that's not in the
|
||
|
// exception list.
|
||
|
do {
|
||
|
if (!$this->recurIterator->valid()) {
|
||
|
$nextDate = null;
|
||
|
break;
|
||
|
}
|
||
|
$nextDate = $this->recurIterator->current();
|
||
|
$this->recurIterator->next();
|
||
|
} while (isset($this->exceptions[$nextDate->getTimeStamp()]));
|
||
|
}
|
||
|
|
||
|
// $nextDate now contains what rrule thinks is the next one, but an
|
||
|
// overridden event may cut ahead.
|
||
|
if ($this->overriddenEventsIndex) {
|
||
|
$offsets = end($this->overriddenEventsIndex);
|
||
|
$timestamp = key($this->overriddenEventsIndex);
|
||
|
$offset = end($offsets);
|
||
|
if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
|
||
|
// Overridden event comes first.
|
||
|
$this->currentOverriddenEvent = $this->overriddenEvents[$offset];
|
||
|
|
||
|
// Putting the rrule next date aside.
|
||
|
$this->nextDate = $nextDate;
|
||
|
$this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
|
||
|
|
||
|
// Ensuring that this item will only be used once.
|
||
|
array_pop($this->overriddenEventsIndex[$timestamp]);
|
||
|
if (!$this->overriddenEventsIndex[$timestamp]) {
|
||
|
array_pop($this->overriddenEventsIndex);
|
||
|
}
|
||
|
|
||
|
// Exit point!
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->currentDate = $nextDate;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Quickly jump to a date in the future.
|
||
|
*
|
||
|
* @param DateTimeInterface $dateTime
|
||
|
*/
|
||
|
public function fastForward(DateTimeInterface $dateTime)
|
||
|
{
|
||
|
while ($this->valid() && $this->getDtEnd() <= $dateTime) {
|
||
|
$this->next();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if this recurring event never ends.
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public function isInfinite()
|
||
|
{
|
||
|
return $this->recurIterator->isInfinite();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* RRULE parser.
|
||
|
*
|
||
|
* @var RRuleIterator
|
||
|
*/
|
||
|
protected $recurIterator;
|
||
|
|
||
|
/**
|
||
|
* The duration, in seconds, of the master event.
|
||
|
*
|
||
|
* We use this to calculate the DTEND for subsequent events.
|
||
|
*/
|
||
|
protected $eventDuration;
|
||
|
|
||
|
/**
|
||
|
* A reference to the main (master) event.
|
||
|
*
|
||
|
* @var VEVENT
|
||
|
*/
|
||
|
protected $masterEvent;
|
||
|
|
||
|
/**
|
||
|
* List of overridden events.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $overriddenEvents = [];
|
||
|
|
||
|
/**
|
||
|
* Overridden event index.
|
||
|
*
|
||
|
* Key is timestamp, value is the list of indexes of the item in the $overriddenEvent
|
||
|
* property.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $overriddenEventsIndex;
|
||
|
|
||
|
/**
|
||
|
* A list of recurrence-id's that are either part of EXDATE, or are
|
||
|
* overridden.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $exceptions = [];
|
||
|
|
||
|
/**
|
||
|
* Internal event counter.
|
||
|
*
|
||
|
* @var int
|
||
|
*/
|
||
|
protected $counter;
|
||
|
|
||
|
/**
|
||
|
* The very start of the iteration process.
|
||
|
*
|
||
|
* @var DateTimeImmutable
|
||
|
*/
|
||
|
protected $startDate;
|
||
|
|
||
|
/**
|
||
|
* Where we are currently in the iteration process.
|
||
|
*
|
||
|
* @var DateTimeImmutable
|
||
|
*/
|
||
|
protected $currentDate;
|
||
|
|
||
|
/**
|
||
|
* The next date from the rrule parser.
|
||
|
*
|
||
|
* Sometimes we need to temporary store the next date, because an
|
||
|
* overridden event came before.
|
||
|
*
|
||
|
* @var DateTimeImmutable
|
||
|
*/
|
||
|
protected $nextDate;
|
||
|
|
||
|
/**
|
||
|
* The event that overwrites the current iteration.
|
||
|
*
|
||
|
* @var VEVENT
|
||
|
*/
|
||
|
protected $currentOverriddenEvent;
|
||
|
}
|