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) 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        $recurid = clone $event->DTSTART;
282        $recurid->name = 'RECURRENCE-ID';
283        $event->add($recurid);
284        return $event;
285
286    }
287
288    /**
289     * Returns the current position of the iterator.
290     *
291     * This is for us simply a 0-based index.
292     *
293     * @return int
294     */
295    public function key() {
296
297        // The counter is always 1 ahead.
298        return $this->counter - 1;
299
300    }
301
302    /**
303     * This is called after next, to see if the iterator is still at a valid
304     * position, or if it's at the end.
305     *
306     * @return bool
307     */
308    public function valid() {
309
310        return !!$this->currentDate;
311
312    }
313
314    /**
315     * Sets the iterator back to the starting point.
316     */
317    public function rewind() {
318
319        $this->recurIterator->rewind();
320        // re-creating overridden event index.
321        $index = array();
322        foreach($this->overriddenEvents as $key=>$event) {
323            $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
324            $index[$stamp] = $key;
325        }
326        krsort($index);
327        $this->counter = 0;
328        $this->overriddenEventsIndex = $index;
329        $this->currentOverriddenEvent = null;
330
331        $this->nextDate = null;
332        $this->currentDate = clone $this->startDate;
333
334        $this->next();
335
336    }
337
338    /**
339     * Advances the iterator with one step.
340     *
341     * @return void
342     */
343    public function next() {
344
345        $this->currentOverriddenEvent = null;
346        $this->counter++;
347        if ($this->nextDate) {
348            // We had a stored value.
349            $nextDate = $this->nextDate;
350            $this->nextDate = null;
351        } else {
352            // We need to ask rruleparser for the next date.
353            // We need to do this until we find a date that's not in the
354            // exception list.
355            do {
356                if (!$this->recurIterator->valid()) {
357                    $nextDate = null;
358                    break;
359                }
360                $nextDate = $this->recurIterator->current();
361                $this->recurIterator->next();
362            } while(isset($this->exceptions[$nextDate->getTimeStamp()]));
363
364        }
365
366
367        // $nextDate now contains what rrule thinks is the next one, but an
368        // overridden event may cut ahead.
369        if ($this->overriddenEventsIndex) {
370
371            $offset = end($this->overriddenEventsIndex);
372            $timestamp = key($this->overriddenEventsIndex);
373            if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
374                // Overridden event comes first.
375                $this->currentOverriddenEvent = $this->overriddenEvents[$offset];
376
377                // Putting the rrule next date aside.
378                $this->nextDate = $nextDate;
379                $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
380
381                // Ensuring that this item will only be used once.
382                array_pop($this->overriddenEventsIndex);
383
384                // Exit point!
385                return;
386
387            }
388
389        }
390
391        $this->currentDate = $nextDate;
392
393    }
394
395    /**
396     * Quickly jump to a date in the future.
397     *
398     * @param DateTime $dateTime
399     */
400    public function fastForward(DateTime $dateTime) {
401
402        while($this->valid() && $this->getDtEnd() < $dateTime ) {
403            $this->next();
404        }
405
406    }
407
408    /**
409     * Returns true if this recurring event never ends.
410     *
411     * @return bool
412     */
413    public function isInfinite() {
414
415        return $this->recurIterator->isInfinite();
416
417    }
418
419    /**
420     * RRULE parser
421     *
422     * @var RRuleIterator
423     */
424    protected $recurIterator;
425
426    /**
427     * The duration, in seconds, of the master event.
428     *
429     * We use this to calculate the DTEND for subsequent events.
430     */
431    protected $eventDuration;
432
433    /**
434     * A reference to the main (master) event.
435     *
436     * @var VEVENT
437     */
438    protected $masterEvent;
439
440    /**
441     * List of overridden events.
442     *
443     * @var array
444     */
445    protected $overriddenEvents = array();
446
447    /**
448     * Overridden event index.
449     *
450     * Key is timestamp, value is the index of the item in the $overriddenEvent
451     * property.
452     *
453     * @var array
454     */
455    protected $overriddenEventsIndex;
456
457    /**
458     * A list of recurrence-id's that are either part of EXDATE, or are
459     * overridden.
460     *
461     * @var array
462     */
463    protected $exceptions = array();
464
465    /**
466     * Internal event counter
467     *
468     * @var int
469     */
470    protected $counter;
471
472    /**
473     * The very start of the iteration process.
474     *
475     * @var DateTime
476     */
477    protected $startDate;
478
479    /**
480     * Where we are currently in the iteration process
481     *
482     * @var DateTime
483     */
484    protected $currentDate;
485
486    /**
487     * The next date from the rrule parser.
488     *
489     * Sometimes we need to temporary store the next date, because an
490     * overridden event came before.
491     *
492     * @var DateTime
493     */
494    protected $nextDate;
495
496}
497