EventIterator.php 13 KB

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