1<?php
2
3namespace Sabre\VObject\Recur;
4
5use DateTimeImmutable;
6use DateTimeInterface;
7use DateTimeZone;
8use InvalidArgumentException;
9use Sabre\VObject\Component;
10use Sabre\VObject\Component\VEvent;
11use Sabre\VObject\Settings;
12
13/**
14 * This class is used to determine new for a recurring event, when the next
15 * events occur.
16 *
17 * This iterator may loop infinitely in the future, therefore it is important
18 * that if you use this class, you set hard limits for the amount of iterations
19 * you want to handle.
20 *
21 * Note that currently there is not full support for the entire iCalendar
22 * specification, as it's very complex and contains a lot of permutations
23 * that's not yet used very often in software.
24 *
25 * For the focus has been on features as they actually appear in Calendaring
26 * software, but this may well get expanded as needed / on demand
27 *
28 * The following RRULE properties are supported
29 *   * UNTIL
30 *   * INTERVAL
31 *   * COUNT
32 *   * FREQ=DAILY
33 *     * BYDAY
34 *     * BYHOUR
35 *     * BYMONTH
36 *   * FREQ=WEEKLY
37 *     * BYDAY
38 *     * BYHOUR
39 *     * WKST
40 *   * FREQ=MONTHLY
41 *     * BYMONTHDAY
42 *     * BYDAY
43 *     * BYSETPOS
44 *   * FREQ=YEARLY
45 *     * BYMONTH
46 *     * BYYEARDAY
47 *     * BYWEEKNO
48 *     * BYMONTHDAY (only if BYMONTH is also set)
49 *     * BYDAY (only if BYMONTH is also set)
50 *
51 * Anything beyond this is 'undefined', which means that it may get ignored, or
52 * you may get unexpected results. The effect is that in some applications the
53 * specified recurrence may look incorrect, or is missing.
54 *
55 * The recurrence iterator also does not yet support THISANDFUTURE.
56 *
57 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
58 * @author Evert Pot (http://evertpot.com/)
59 * @license http://sabre.io/license/ Modified BSD License
60 */
61class EventIterator implements \Iterator {
62
63    /**
64     * Reference timeZone for floating dates and times.
65     *
66     * @var DateTimeZone
67     */
68    protected $timeZone;
69
70    /**
71     * True if we're iterating an all-day event.
72     *
73     * @var bool
74     */
75    protected $allDay = false;
76
77    /**
78     * Creates the iterator.
79     *
80     * There's three ways to set up the iterator.
81     *
82     * 1. You can pass a VCALENDAR component and a UID.
83     * 2. You can pass an array of VEVENTs (all UIDS should match).
84     * 3. You can pass a single VEVENT component.
85     *
86     * Only the second method is recomended. The other 1 and 3 will be removed
87     * at some point in the future.
88     *
89     * The $uid parameter is only required for the first method.
90     *
91     * @param Component|array $input
92     * @param string|null $uid
93     * @param DateTimeZone $timeZone Reference timezone for floating dates and
94     *                               times.
95     */
96    function __construct($input, $uid = null, DateTimeZone $timeZone = null) {
97
98        if (is_null($timeZone)) {
99            $timeZone = new DateTimeZone('UTC');
100        }
101        $this->timeZone = $timeZone;
102
103        if (is_array($input)) {
104            $events = $input;
105        } elseif ($input instanceof VEvent) {
106            // Single instance mode.
107            $events = [$input];
108        } else {
109            // Calendar + UID mode.
110            $uid = (string)$uid;
111            if (!$uid) {
112                throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
113            }
114            if (!isset($input->VEVENT)) {
115                throw new InvalidArgumentException('No events found in this calendar');
116            }
117            $events = $input->getByUID($uid);
118
119        }
120
121        foreach ($events as $vevent) {
122
123            if (!isset($vevent->{'RECURRENCE-ID'})) {
124
125                $this->masterEvent = $vevent;
126
127            } else {
128
129                $this->exceptions[
130                    $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
131                ] = true;
132                $this->overriddenEvents[] = $vevent;
133
134            }
135
136        }
137
138        if (!$this->masterEvent) {
139            // No base event was found. CalDAV does allow cases where only
140            // overridden instances are stored.
141            //
142            // In this particular case, we're just going to grab the first
143            // event and use that instead. This may not always give the
144            // desired result.
145            if (!count($this->overriddenEvents)) {
146                throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid);
147            }
148            $this->masterEvent = array_shift($this->overriddenEvents);
149        }
150
151        $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone);
152        $this->allDay = !$this->masterEvent->DTSTART->hasTime();
153
154        if (isset($this->masterEvent->EXDATE)) {
155
156            foreach ($this->masterEvent->EXDATE as $exDate) {
157
158                foreach ($exDate->getDateTimes($this->timeZone) as $dt) {
159                    $this->exceptions[$dt->getTimeStamp()] = true;
160                }
161
162            }
163
164        }
165
166        if (isset($this->masterEvent->DTEND)) {
167            $this->eventDuration =
168                $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() -
169                $this->startDate->getTimeStamp();
170        } elseif (isset($this->masterEvent->DURATION)) {
171            $duration = $this->masterEvent->DURATION->getDateInterval();
172            $end = clone $this->startDate;
173            $end = $end->add($duration);
174            $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp();
175        } elseif ($this->allDay) {
176            $this->eventDuration = 3600 * 24;
177        } else {
178            $this->eventDuration = 0;
179        }
180
181        if (isset($this->masterEvent->RDATE)) {
182            $this->recurIterator = new RDateIterator(
183                $this->masterEvent->RDATE->getParts(),
184                $this->startDate
185            );
186        } elseif (isset($this->masterEvent->RRULE)) {
187            $this->recurIterator = new RRuleIterator(
188                $this->masterEvent->RRULE->getParts(),
189                $this->startDate
190            );
191        } else {
192            $this->recurIterator = new RRuleIterator(
193                [
194                    'FREQ'  => 'DAILY',
195                    'COUNT' => 1,
196                ],
197                $this->startDate
198            );
199        }
200
201        $this->rewind();
202        if (!$this->valid()) {
203            throw new NoInstancesException('This recurrence rule does not generate any valid instances');
204        }
205
206    }
207
208    /**
209     * Returns the date for the current position of the iterator.
210     *
211     * @return DateTimeImmutable
212     */
213    function current() {
214
215        if ($this->currentDate) {
216            return clone $this->currentDate;
217        }
218
219    }
220
221    /**
222     * This method returns the start date for the current iteration of the
223     * event.
224     *
225     * @return DateTimeImmutable
226     */
227    function getDtStart() {
228
229        if ($this->currentDate) {
230            return clone $this->currentDate;
231        }
232
233    }
234
235    /**
236     * This method returns the end date for the current iteration of the
237     * event.
238     *
239     * @return DateTimeImmutable
240     */
241    function getDtEnd() {
242
243        if (!$this->valid()) {
244            return;
245        }
246        $end = clone $this->currentDate;
247        return $end->modify('+' . $this->eventDuration . ' seconds');
248
249    }
250
251    /**
252     * Returns a VEVENT for the current iterations of the event.
253     *
254     * This VEVENT will have a recurrence id, and it's DTSTART and DTEND
255     * altered.
256     *
257     * @return VEvent
258     */
259    function getEventObject() {
260
261        if ($this->currentOverriddenEvent) {
262            return $this->currentOverriddenEvent;
263        }
264
265        $event = clone $this->masterEvent;
266
267        // Ignoring the following block, because PHPUnit's code coverage
268        // ignores most of these lines, and this messes with our stats.
269        //
270        // @codeCoverageIgnoreStart
271        unset(
272            $event->RRULE,
273            $event->EXDATE,
274            $event->RDATE,
275            $event->EXRULE,
276            $event->{'RECURRENCE-ID'}
277        );
278        // @codeCoverageIgnoreEnd
279
280        $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating());
281        if (isset($event->DTEND)) {
282            $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating());
283        }
284        $recurid = clone $event->DTSTART;
285        $recurid->name = 'RECURRENCE-ID';
286        $event->add($recurid);
287        return $event;
288
289    }
290
291    /**
292     * Returns the current position of the iterator.
293     *
294     * This is for us simply a 0-based index.
295     *
296     * @return int
297     */
298    function key() {
299
300        // The counter is always 1 ahead.
301        return $this->counter - 1;
302
303    }
304
305    /**
306     * This is called after next, to see if the iterator is still at a valid
307     * position, or if it's at the end.
308     *
309     * @return bool
310     */
311    function valid() {
312
313        if ($this->counter > Settings::$maxRecurrences && Settings::$maxRecurrences !== -1) {
314            throw new MaxInstancesExceededException('Recurring events are only allowed to generate ' . Settings::$maxRecurrences);
315        }
316        return !!$this->currentDate;
317
318    }
319
320    /**
321     * Sets the iterator back to the starting point.
322     */
323    function rewind() {
324
325        $this->recurIterator->rewind();
326        // re-creating overridden event index.
327        $index = [];
328        foreach ($this->overriddenEvents as $key => $event) {
329            $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
330            $index[$stamp][] = $key;
331        }
332        krsort($index);
333        $this->counter = 0;
334        $this->overriddenEventsIndex = $index;
335        $this->currentOverriddenEvent = null;
336
337        $this->nextDate = null;
338        $this->currentDate = clone $this->startDate;
339
340        $this->next();
341
342    }
343
344    /**
345     * Advances the iterator with one step.
346     *
347     * @return void
348     */
349    function next() {
350
351        $this->currentOverriddenEvent = null;
352        $this->counter++;
353        if ($this->nextDate) {
354            // We had a stored value.
355            $nextDate = $this->nextDate;
356            $this->nextDate = null;
357        } else {
358            // We need to ask rruleparser for the next date.
359            // We need to do this until we find a date that's not in the
360            // exception list.
361            do {
362                if (!$this->recurIterator->valid()) {
363                    $nextDate = null;
364                    break;
365                }
366                $nextDate = $this->recurIterator->current();
367                $this->recurIterator->next();
368            } while (isset($this->exceptions[$nextDate->getTimeStamp()]));
369
370        }
371
372
373        // $nextDate now contains what rrule thinks is the next one, but an
374        // overridden event may cut ahead.
375        if ($this->overriddenEventsIndex) {
376
377            $offsets = end($this->overriddenEventsIndex);
378            $timestamp = key($this->overriddenEventsIndex);
379            $offset = end($offsets);
380            if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
381                // Overridden event comes first.
382                $this->currentOverriddenEvent = $this->overriddenEvents[$offset];
383
384                // Putting the rrule next date aside.
385                $this->nextDate = $nextDate;
386                $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
387
388                // Ensuring that this item will only be used once.
389                array_pop($this->overriddenEventsIndex[$timestamp]);
390                if (!$this->overriddenEventsIndex[$timestamp]) {
391                    array_pop($this->overriddenEventsIndex);
392                }
393
394                // Exit point!
395                return;
396
397            }
398
399        }
400
401        $this->currentDate = $nextDate;
402
403    }
404
405    /**
406     * Quickly jump to a date in the future.
407     *
408     * @param DateTimeInterface $dateTime
409     */
410    function fastForward(DateTimeInterface $dateTime) {
411
412        while ($this->valid() && $this->getDtEnd() <= $dateTime) {
413            $this->next();
414        }
415
416    }
417
418    /**
419     * Returns true if this recurring event never ends.
420     *
421     * @return bool
422     */
423    function isInfinite() {
424
425        return $this->recurIterator->isInfinite();
426
427    }
428
429    /**
430     * RRULE parser.
431     *
432     * @var RRuleIterator
433     */
434    protected $recurIterator;
435
436    /**
437     * The duration, in seconds, of the master event.
438     *
439     * We use this to calculate the DTEND for subsequent events.
440     */
441    protected $eventDuration;
442
443    /**
444     * A reference to the main (master) event.
445     *
446     * @var VEVENT
447     */
448    protected $masterEvent;
449
450    /**
451     * List of overridden events.
452     *
453     * @var array
454     */
455    protected $overriddenEvents = [];
456
457    /**
458     * Overridden event index.
459     *
460     * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent
461     * property.
462     *
463     * @var array
464     */
465    protected $overriddenEventsIndex;
466
467    /**
468     * A list of recurrence-id's that are either part of EXDATE, or are
469     * overridden.
470     *
471     * @var array
472     */
473    protected $exceptions = [];
474
475    /**
476     * Internal event counter.
477     *
478     * @var int
479     */
480    protected $counter;
481
482    /**
483     * The very start of the iteration process.
484     *
485     * @var DateTimeImmutable
486     */
487    protected $startDate;
488
489    /**
490     * Where we are currently in the iteration process.
491     *
492     * @var DateTimeImmutable
493     */
494    protected $currentDate;
495
496    /**
497     * The next date from the rrule parser.
498     *
499     * Sometimes we need to temporary store the next date, because an
500     * overridden event came before.
501     *
502     * @var DateTimeImmutable
503     */
504    protected $nextDate;
505
506    /**
507     * The event that overwrites the current iteration
508     *
509     * @var VEVENT
510     */
511    protected $currentOverriddenEvent;
512
513}
514