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