123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 |
- <?php
- namespace Sabre\VObject\Recur;
- use InvalidArgumentException;
- use DateTime;
- use DateTimeZone;
- use Sabre\VObject\Component;
- use Sabre\VObject\Component\VEvent;
- /**
- * 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.
- */
- public function __construct($input, $uid = null, DateTimeZone $timeZone = null) {
- if (is_null($this->timeZone)) {
- $timeZone = new DateTimeZone('UTC');
- }
- $this->timeZone = $timeZone;
- if (is_array($input)) {
- $events = $input;
- } elseif ($input instanceof VEvent) {
- // Single instance mode.
- $events = array($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->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(
- array(
- '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 DateTime
- */
- public function current() {
- if ($this->currentDate) {
- return clone $this->currentDate;
- }
- }
- /**
- * This method returns the start date for the current iteration of the
- * event.
- *
- * @return DateTime
- */
- public function getDtStart() {
- if ($this->currentDate) {
- return clone $this->currentDate;
- }
- }
- /**
- * This method returns the end date for the current iteration of the
- * event.
- *
- * @return DateTime
- */
- public function getDtEnd() {
- if (!$this->valid()) {
- return null;
- }
- $end = clone $this->currentDate;
- $end->modify('+' . $this->eventDuration . ' seconds');
- return $end;
- }
- /**
- * 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
- */
- 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() {
- return !!$this->currentDate;
- }
- /**
- * Sets the iterator back to the starting point.
- */
- public function rewind() {
- $this->recurIterator->rewind();
- // re-creating overridden event index.
- $index = array();
- 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
- */
- 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 DateTime $dateTime
- */
- public function fastForward(DateTime $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 = array();
- /**
- * 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 = array();
- /**
- * Internal event counter
- *
- * @var int
- */
- protected $counter;
- /**
- * The very start of the iteration process.
- *
- * @var DateTime
- */
- protected $startDate;
- /**
- * Where we are currently in the iteration process
- *
- * @var DateTime
- */
- 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 DateTime
- */
- protected $nextDate;
- }
|