EventIterator.php 14 KB

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