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