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