aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/sabre/vobject/lib/Recur/EventIterator.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/sabre/vobject/lib/Recur/EventIterator.php')
-rw-r--r--vendor/sabre/vobject/lib/Recur/EventIterator.php507
1 files changed, 507 insertions, 0 deletions
diff --git a/vendor/sabre/vobject/lib/Recur/EventIterator.php b/vendor/sabre/vobject/lib/Recur/EventIterator.php
new file mode 100644
index 000000000..86c996aec
--- /dev/null
+++ b/vendor/sabre/vobject/lib/Recur/EventIterator.php
@@ -0,0 +1,507 @@
+<?php
+
+namespace Sabre\VObject\Recur;
+
+use DateTimeZone;
+use DateTimeImmutable;
+use DateTimeInterface;
+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
+ * * 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.
+ */
+ 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
+ */
+ function current() {
+
+ if ($this->currentDate) {
+ return clone $this->currentDate;
+ }
+
+ }
+
+ /**
+ * This method returns the start date for the current iteration of the
+ * event.
+ *
+ * @return DateTimeImmutable
+ */
+ function getDtStart() {
+
+ if ($this->currentDate) {
+ return clone $this->currentDate;
+ }
+
+ }
+
+ /**
+ * This method returns the end date for the current iteration of the
+ * event.
+ *
+ * @return DateTimeImmutable
+ */
+ 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 it's DTSTART and DTEND
+ * altered.
+ *
+ * @return VEvent
+ */
+ 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
+ */
+ 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
+ */
+ function valid() {
+
+ if ($this->counter > Settings::$maxRecurrences && Settings::$maxRecurrences !== -1) {
+ throw new MaxInstancesExceededException('Recurring events are only allowed to generate ' . Settings::$maxRecurrences);
+ }
+ return !!$this->currentDate;
+
+ }
+
+ /**
+ * Sets the iterator back to the starting point.
+ */
+ 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.
+ *
+ * @return void
+ */
+ 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) {
+
+ $offset = end($this->overriddenEventsIndex);
+ $timestamp = key($this->overriddenEventsIndex);
+ 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);
+
+ // Exit point!
+ return;
+
+ }
+
+ }
+
+ $this->currentDate = $nextDate;
+
+ }
+
+ /**
+ * Quickly jump to a date in the future.
+ *
+ * @param DateTimeInterface $dateTime
+ */
+ function fastForward(DateTimeInterface $dateTime) {
+
+ while ($this->valid() && $this->getDtEnd() < $dateTime) {
+ $this->next();
+ }
+
+ }
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ 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 index 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;
+
+}