EventIterator.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  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) 2007-2014 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. * You should pass a VCALENDAR component, as well as the UID of the event
  71. * we're going to traverse.
  72. *
  73. * @param Component $vcal
  74. * @param string|null $uid
  75. * @param DateTimeZone $timeZone Reference timezone for floating dates and
  76. * times.
  77. */
  78. public function __construct(Component $vcal, $uid = null, DateTimeZone $timeZone = null) {
  79. if (is_null($this->timeZone)) {
  80. $timeZone = new DateTimeZone('UTC');
  81. }
  82. $this->timeZone = $timeZone;
  83. $rrule = null;
  84. if ($vcal instanceof VEvent) {
  85. // Single instance mode.
  86. $events = array($vcal);
  87. } else {
  88. $uid = (string)$uid;
  89. if (!$uid) {
  90. throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor');
  91. }
  92. if (!isset($vcal->VEVENT)) {
  93. throw new InvalidArgumentException('No events found in this calendar');
  94. }
  95. $events = array();
  96. foreach($vcal->VEVENT as $event) {
  97. if ($event->uid->getValue() === $uid) {
  98. $events[] = $event;
  99. }
  100. }
  101. }
  102. foreach($events as $vevent) {
  103. if (!isset($vevent->{'RECURRENCE-ID'})) {
  104. $this->masterEvent = $vevent;
  105. } else {
  106. $this->exceptions[
  107. $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp()
  108. ] = true;
  109. $this->overriddenEvents[] = $vevent;
  110. }
  111. }
  112. if (!$this->masterEvent) {
  113. // No base event was found. CalDAV does allow cases where only
  114. // overridden instances are stored.
  115. //
  116. // In this particular case, we're just going to grab the first
  117. // event and use that instead. This may not always give the
  118. // desired result.
  119. if (!count($this->overriddenEvents)) {
  120. throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid);
  121. }
  122. $this->masterEvent = array_shift($this->overriddenEvents);
  123. }
  124. // master event.
  125. if (isset($this->masterEvent->RRULE)) {
  126. $rrule = $this->masterEvent->RRULE->getParts();
  127. } else {
  128. // master event has no rrule. We default to something that
  129. // iterates once.
  130. $rrule = array(
  131. 'FREQ' => 'DAILY',
  132. 'COUNT' => 1,
  133. );
  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->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. array(
  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 DateTime
  186. */
  187. public function current() {
  188. if ($this->currentDate) {
  189. return clone $this->currentDate;
  190. }
  191. }
  192. /**
  193. * This method returns the start date for the current iteration of the
  194. * event.
  195. *
  196. * @return DateTime
  197. */
  198. public function getDtStart() {
  199. if ($this->currentDate) {
  200. return clone $this->currentDate;
  201. }
  202. }
  203. /**
  204. * This method returns the end date for the current iteration of the
  205. * event.
  206. *
  207. * @return DateTime
  208. */
  209. public function getDtEnd() {
  210. if (!$this->valid()) {
  211. return null;
  212. }
  213. $end = clone $this->currentDate;
  214. $end->modify('+' . $this->eventDuration . ' seconds');
  215. return $end;
  216. }
  217. /**
  218. * Returns a VEVENT for the current iterations of the event.
  219. *
  220. * This VEVENT will have a recurrence id, and it's DTSTART and DTEND
  221. * altered.
  222. *
  223. * @return VEvent
  224. */
  225. public function getEventObject() {
  226. if ($this->currentOverriddenEvent) {
  227. return $this->currentOverriddenEvent;
  228. }
  229. $event = clone $this->masterEvent;
  230. // Ignoring the following block, because PHPUnit's code coverage
  231. // ignores most of these lines, and this messes with our stats.
  232. //
  233. // @codeCoverageIgnoreStart
  234. unset(
  235. $event->RRULE,
  236. $event->EXDATE,
  237. $event->RDATE,
  238. $event->EXRULE,
  239. $event->{'RECURRENCE-ID'}
  240. );
  241. // @codeCoverageIgnoreEnd
  242. $event->DTSTART->setDateTime($this->getDtStart());
  243. if (isset($event->DTEND)) {
  244. $event->DTEND->setDateTime($this->getDtEnd());
  245. }
  246. // Including a RECURRENCE-ID to the object, unless this is the first
  247. // object.
  248. //
  249. // The inner recurIterator is always one step ahead, this is why we're
  250. // checking for the key being higher than 1.
  251. if ($this->recurIterator->key() > 1) {
  252. $recurid = clone $event->DTSTART;
  253. $recurid->name = 'RECURRENCE-ID';
  254. $event->add($recurid);
  255. }
  256. return $event;
  257. }
  258. /**
  259. * Returns the current position of the iterator.
  260. *
  261. * This is for us simply a 0-based index.
  262. *
  263. * @return int
  264. */
  265. public function key() {
  266. // The counter is always 1 ahead.
  267. return $this->counter - 1;
  268. }
  269. /**
  270. * This is called after next, to see if the iterator is still at a valid
  271. * position, or if it's at the end.
  272. *
  273. * @return bool
  274. */
  275. public function valid() {
  276. return !!$this->currentDate;
  277. }
  278. /**
  279. * Sets the iterator back to the starting point.
  280. */
  281. public function rewind() {
  282. $this->recurIterator->rewind();
  283. // re-creating overridden event index.
  284. $index = array();
  285. foreach($this->overriddenEvents as $key=>$event) {
  286. $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp();
  287. $index[$stamp] = $key;
  288. }
  289. krsort($index);
  290. $this->counter = 0;
  291. $this->overriddenEventsIndex = $index;
  292. $this->currentOverriddenEvent = null;
  293. $this->nextDate = null;
  294. $this->currentDate = clone $this->startDate;
  295. $this->next();
  296. }
  297. /**
  298. * Advances the iterator with one step.
  299. *
  300. * @return void
  301. */
  302. public function next() {
  303. $this->currentOverriddenEvent = null;
  304. $this->counter++;
  305. if ($this->nextDate) {
  306. // We had a stored value.
  307. $nextDate = $this->nextDate;
  308. $this->nextDate = null;
  309. } else {
  310. // We need to ask rruleparser for the next date.
  311. // We need to do this until we find a date that's not in the
  312. // exception list.
  313. do {
  314. if (!$this->recurIterator->valid()) {
  315. $nextDate = null;
  316. break;
  317. }
  318. $nextDate = $this->recurIterator->current();
  319. $this->recurIterator->next();
  320. } while(isset($this->exceptions[$nextDate->getTimeStamp()]));
  321. }
  322. // $nextDate now contains what rrule thinks is the next one, but an
  323. // overridden event may cut ahead.
  324. if ($this->overriddenEventsIndex) {
  325. $offset = end($this->overriddenEventsIndex);
  326. $timestamp = key($this->overriddenEventsIndex);
  327. if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) {
  328. // Overridden event comes first.
  329. $this->currentOverriddenEvent = $this->overriddenEvents[$offset];
  330. // Putting the rrule next date aside.
  331. $this->nextDate = $nextDate;
  332. $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone);
  333. // Ensuring that this item will only be used once.
  334. array_pop($this->overriddenEventsIndex);
  335. // Exit point!
  336. return;
  337. }
  338. }
  339. $this->currentDate = $nextDate;
  340. }
  341. /**
  342. * Quickly jump to a date in the future.
  343. *
  344. * @param DateTime $dateTime
  345. */
  346. public function fastForward(DateTime $dateTime) {
  347. while($this->valid() && $this->getDtEnd() < $dateTime ) {
  348. $this->next();
  349. }
  350. }
  351. /**
  352. * Returns true if this recurring event never ends.
  353. *
  354. * @return bool
  355. */
  356. public function isInfinite() {
  357. return $this->recurIterator->isInfinite();
  358. }
  359. /**
  360. * RRULE parser
  361. *
  362. * @var RRuleIterator
  363. */
  364. protected $recurIterator;
  365. /**
  366. * The duration, in seconds, of the master event.
  367. *
  368. * We use this to calculate the DTEND for subsequent events.
  369. */
  370. protected $eventDuration;
  371. /**
  372. * A reference to the main (master) event.
  373. *
  374. * @var VEVENT
  375. */
  376. protected $masterEvent;
  377. /**
  378. * List of overridden events.
  379. *
  380. * @var array
  381. */
  382. protected $overriddenEvents = array();
  383. /**
  384. * Overridden event index.
  385. *
  386. * Key is timestamp, value is the index of the item in the $overriddenEvent
  387. * property.
  388. *
  389. * @var array
  390. */
  391. protected $overriddenEventsIndex;
  392. /**
  393. * A list of recurrence-id's that are either part of EXDATE, or are
  394. * overridden.
  395. *
  396. * @var array
  397. */
  398. protected $exceptions = array();
  399. /**
  400. * Internal event counter
  401. *
  402. * @var int
  403. */
  404. protected $counter;
  405. /**
  406. * The very start of the iteration process.
  407. *
  408. * @var DateTime
  409. */
  410. protected $startDate;
  411. /**
  412. * Where we are currently in the iteration process
  413. *
  414. * @var DateTime
  415. */
  416. protected $currentDate;
  417. /**
  418. * The next date from the rrule parser.
  419. *
  420. * Sometimes we need to temporary store the next date, because an
  421. * overridden event came before.
  422. *
  423. * @var DateTime
  424. */
  425. protected $nextDate;
  426. }