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