VCalendar.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. <?php
  2. namespace Sabre\VObject\Component;
  3. use DateTimeInterface;
  4. use DateTimeZone;
  5. use Sabre\VObject;
  6. use Sabre\VObject\Component;
  7. use Sabre\VObject\InvalidDataException;
  8. use Sabre\VObject\Property;
  9. use Sabre\VObject\Recur\EventIterator;
  10. use Sabre\VObject\Recur\NoInstancesException;
  11. /**
  12. * The VCalendar component.
  13. *
  14. * This component adds functionality to a component, specific for a VCALENDAR.
  15. *
  16. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  17. * @author Evert Pot (http://evertpot.com/)
  18. * @license http://sabre.io/license/ Modified BSD License
  19. */
  20. class VCalendar extends VObject\Document
  21. {
  22. /**
  23. * The default name for this component.
  24. *
  25. * This should be 'VCALENDAR' or 'VCARD'.
  26. *
  27. * @var string
  28. */
  29. public static $defaultName = 'VCALENDAR';
  30. /**
  31. * This is a list of components, and which classes they should map to.
  32. *
  33. * @var array
  34. */
  35. public static $componentMap = [
  36. 'VCALENDAR' => self::class,
  37. 'VALARM' => VAlarm::class,
  38. 'VEVENT' => VEvent::class,
  39. 'VFREEBUSY' => VFreeBusy::class,
  40. 'VAVAILABILITY' => VAvailability::class,
  41. 'AVAILABLE' => Available::class,
  42. 'VJOURNAL' => VJournal::class,
  43. 'VTIMEZONE' => VTimeZone::class,
  44. 'VTODO' => VTodo::class,
  45. ];
  46. /**
  47. * List of value-types, and which classes they map to.
  48. *
  49. * @var array
  50. */
  51. public static $valueMap = [
  52. 'BINARY' => VObject\Property\Binary::class,
  53. 'BOOLEAN' => VObject\Property\Boolean::class,
  54. 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class,
  55. 'DATE' => VObject\Property\ICalendar\Date::class,
  56. 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class,
  57. 'DURATION' => VObject\Property\ICalendar\Duration::class,
  58. 'FLOAT' => VObject\Property\FloatValue::class,
  59. 'INTEGER' => VObject\Property\IntegerValue::class,
  60. 'PERIOD' => VObject\Property\ICalendar\Period::class,
  61. 'RECUR' => VObject\Property\ICalendar\Recur::class,
  62. 'TEXT' => VObject\Property\Text::class,
  63. 'TIME' => VObject\Property\Time::class,
  64. 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
  65. 'URI' => VObject\Property\Uri::class,
  66. 'UTC-OFFSET' => VObject\Property\UtcOffset::class,
  67. ];
  68. /**
  69. * List of properties, and which classes they map to.
  70. *
  71. * @var array
  72. */
  73. public static $propertyMap = [
  74. // Calendar properties
  75. 'CALSCALE' => VObject\Property\FlatText::class,
  76. 'METHOD' => VObject\Property\FlatText::class,
  77. 'PRODID' => VObject\Property\FlatText::class,
  78. 'VERSION' => VObject\Property\FlatText::class,
  79. // Component properties
  80. 'ATTACH' => VObject\Property\Uri::class,
  81. 'CATEGORIES' => VObject\Property\Text::class,
  82. 'CLASS' => VObject\Property\FlatText::class,
  83. 'COMMENT' => VObject\Property\FlatText::class,
  84. 'DESCRIPTION' => VObject\Property\FlatText::class,
  85. 'GEO' => VObject\Property\FloatValue::class,
  86. 'LOCATION' => VObject\Property\FlatText::class,
  87. 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class,
  88. 'PRIORITY' => VObject\Property\IntegerValue::class,
  89. 'RESOURCES' => VObject\Property\Text::class,
  90. 'STATUS' => VObject\Property\FlatText::class,
  91. 'SUMMARY' => VObject\Property\FlatText::class,
  92. // Date and Time Component Properties
  93. 'COMPLETED' => VObject\Property\ICalendar\DateTime::class,
  94. 'DTEND' => VObject\Property\ICalendar\DateTime::class,
  95. 'DUE' => VObject\Property\ICalendar\DateTime::class,
  96. 'DTSTART' => VObject\Property\ICalendar\DateTime::class,
  97. 'DURATION' => VObject\Property\ICalendar\Duration::class,
  98. 'FREEBUSY' => VObject\Property\ICalendar\Period::class,
  99. 'TRANSP' => VObject\Property\FlatText::class,
  100. // Time Zone Component Properties
  101. 'TZID' => VObject\Property\FlatText::class,
  102. 'TZNAME' => VObject\Property\FlatText::class,
  103. 'TZOFFSETFROM' => VObject\Property\UtcOffset::class,
  104. 'TZOFFSETTO' => VObject\Property\UtcOffset::class,
  105. 'TZURL' => VObject\Property\Uri::class,
  106. // Relationship Component Properties
  107. 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class,
  108. 'CONTACT' => VObject\Property\FlatText::class,
  109. 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class,
  110. 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class,
  111. 'RELATED-TO' => VObject\Property\FlatText::class,
  112. 'URL' => VObject\Property\Uri::class,
  113. 'UID' => VObject\Property\FlatText::class,
  114. // Recurrence Component Properties
  115. 'EXDATE' => VObject\Property\ICalendar\DateTime::class,
  116. 'RDATE' => VObject\Property\ICalendar\DateTime::class,
  117. 'RRULE' => VObject\Property\ICalendar\Recur::class,
  118. 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545
  119. // Alarm Component Properties
  120. 'ACTION' => VObject\Property\FlatText::class,
  121. 'REPEAT' => VObject\Property\IntegerValue::class,
  122. 'TRIGGER' => VObject\Property\ICalendar\Duration::class,
  123. // Change Management Component Properties
  124. 'CREATED' => VObject\Property\ICalendar\DateTime::class,
  125. 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class,
  126. 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class,
  127. 'SEQUENCE' => VObject\Property\IntegerValue::class,
  128. // Request Status
  129. 'REQUEST-STATUS' => VObject\Property\Text::class,
  130. // Additions from draft-daboo-valarm-extensions-04
  131. 'ALARM-AGENT' => VObject\Property\Text::class,
  132. 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class,
  133. 'PROXIMITY' => VObject\Property\Text::class,
  134. 'DEFAULT-ALARM' => VObject\Property\Boolean::class,
  135. // Additions from draft-daboo-calendar-availability-05
  136. 'BUSYTYPE' => VObject\Property\Text::class,
  137. ];
  138. /**
  139. * Returns the current document type.
  140. *
  141. * @return int
  142. */
  143. public function getDocumentType()
  144. {
  145. return self::ICALENDAR20;
  146. }
  147. /**
  148. * Returns a list of all 'base components'. For instance, if an Event has
  149. * a recurrence rule, and one instance is overridden, the overridden event
  150. * will have the same UID, but will be excluded from this list.
  151. *
  152. * VTIMEZONE components will always be excluded.
  153. *
  154. * @param string $componentName filter by component name
  155. *
  156. * @return VObject\Component[]
  157. */
  158. public function getBaseComponents($componentName = null)
  159. {
  160. $isBaseComponent = function ($component) {
  161. if (!$component instanceof VObject\Component) {
  162. return false;
  163. }
  164. if ('VTIMEZONE' === $component->name) {
  165. return false;
  166. }
  167. if (isset($component->{'RECURRENCE-ID'})) {
  168. return false;
  169. }
  170. return true;
  171. };
  172. if ($componentName) {
  173. // Early exit
  174. return array_filter(
  175. $this->select($componentName),
  176. $isBaseComponent
  177. );
  178. }
  179. $components = [];
  180. foreach ($this->children as $childGroup) {
  181. foreach ($childGroup as $child) {
  182. if (!$child instanceof Component) {
  183. // If one child is not a component, they all are so we skip
  184. // the entire group.
  185. continue 2;
  186. }
  187. if ($isBaseComponent($child)) {
  188. $components[] = $child;
  189. }
  190. }
  191. }
  192. return $components;
  193. }
  194. /**
  195. * Returns the first component that is not a VTIMEZONE, and does not have
  196. * an RECURRENCE-ID.
  197. *
  198. * If there is no such component, null will be returned.
  199. *
  200. * @param string $componentName filter by component name
  201. *
  202. * @return VObject\Component|null
  203. */
  204. public function getBaseComponent($componentName = null)
  205. {
  206. $isBaseComponent = function ($component) {
  207. if (!$component instanceof VObject\Component) {
  208. return false;
  209. }
  210. if ('VTIMEZONE' === $component->name) {
  211. return false;
  212. }
  213. if (isset($component->{'RECURRENCE-ID'})) {
  214. return false;
  215. }
  216. return true;
  217. };
  218. if ($componentName) {
  219. foreach ($this->select($componentName) as $child) {
  220. if ($isBaseComponent($child)) {
  221. return $child;
  222. }
  223. }
  224. return null;
  225. }
  226. // Searching all components
  227. foreach ($this->children as $childGroup) {
  228. foreach ($childGroup as $child) {
  229. if ($isBaseComponent($child)) {
  230. return $child;
  231. }
  232. }
  233. }
  234. return null;
  235. }
  236. /**
  237. * Expand all events in this VCalendar object and return a new VCalendar
  238. * with the expanded events.
  239. *
  240. * If this calendar object, has events with recurrence rules, this method
  241. * can be used to expand the event into multiple sub-events.
  242. *
  243. * Each event will be stripped from its recurrence information, and only
  244. * the instances of the event in the specified timerange will be left
  245. * alone.
  246. *
  247. * In addition, this method will cause timezone information to be stripped,
  248. * and normalized to UTC.
  249. *
  250. * @param DateTimeZone $timeZone reference timezone for floating dates and
  251. * times
  252. *
  253. * @return VCalendar
  254. */
  255. public function expand(DateTimeInterface $start, DateTimeInterface $end, ?DateTimeZone $timeZone = null)
  256. {
  257. $newChildren = [];
  258. $recurringEvents = [];
  259. if (!$timeZone) {
  260. $timeZone = new DateTimeZone('UTC');
  261. }
  262. $stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) {
  263. foreach ($component->children() as $componentChild) {
  264. if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) {
  265. $dt = $componentChild->getDateTimes($timeZone);
  266. // We only need to update the first timezone, because
  267. // setDateTimes will match all other timezones to the
  268. // first.
  269. $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC'));
  270. $componentChild->setDateTimes($dt);
  271. } elseif ($componentChild instanceof Component) {
  272. $stripTimezones($componentChild);
  273. }
  274. }
  275. return $component;
  276. };
  277. foreach ($this->children() as $child) {
  278. if ($child instanceof Property && 'PRODID' !== $child->name) {
  279. // We explicitly want to ignore PRODID, because we want to
  280. // overwrite it with our own.
  281. $newChildren[] = clone $child;
  282. } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) {
  283. // We're also stripping all VTIMEZONE objects because we're
  284. // converting everything to UTC.
  285. if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) {
  286. // Handle these a bit later.
  287. $uid = (string) $child->UID;
  288. if (!$uid) {
  289. throw new InvalidDataException('Every VEVENT object must have a UID property');
  290. }
  291. if (isset($recurringEvents[$uid])) {
  292. $recurringEvents[$uid][] = clone $child;
  293. } else {
  294. $recurringEvents[$uid] = [clone $child];
  295. }
  296. } elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) {
  297. $newChildren[] = $stripTimezones(clone $child);
  298. }
  299. }
  300. }
  301. foreach ($recurringEvents as $events) {
  302. try {
  303. $it = new EventIterator($events, null, $timeZone);
  304. } catch (NoInstancesException $e) {
  305. // This event is recurring, but it doesn't have a single
  306. // instance. We are skipping this event from the output
  307. // entirely.
  308. continue;
  309. }
  310. $it->fastForward($start);
  311. while ($it->valid() && $it->getDTStart() < $end) {
  312. if ($it->getDTEnd() > $start) {
  313. $newChildren[] = $stripTimezones($it->getEventObject());
  314. }
  315. $it->next();
  316. }
  317. }
  318. return new self($newChildren);
  319. }
  320. /**
  321. * This method should return a list of default property values.
  322. *
  323. * @return array
  324. */
  325. protected function getDefaults()
  326. {
  327. return [
  328. 'VERSION' => '2.0',
  329. 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
  330. 'CALSCALE' => 'GREGORIAN',
  331. ];
  332. }
  333. /**
  334. * A simple list of validation rules.
  335. *
  336. * This is simply a list of properties, and how many times they either
  337. * must or must not appear.
  338. *
  339. * Possible values per property:
  340. * * 0 - Must not appear.
  341. * * 1 - Must appear exactly once.
  342. * * + - Must appear at least once.
  343. * * * - Can appear any number of times.
  344. * * ? - May appear, but not more than once.
  345. *
  346. * @var array
  347. */
  348. public function getValidationRules()
  349. {
  350. return [
  351. 'PRODID' => 1,
  352. 'VERSION' => 1,
  353. 'CALSCALE' => '?',
  354. 'METHOD' => '?',
  355. ];
  356. }
  357. /**
  358. * Validates the node for correctness.
  359. *
  360. * The following options are supported:
  361. * Node::REPAIR - May attempt to automatically repair the problem.
  362. * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
  363. * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
  364. *
  365. * This method returns an array with detected problems.
  366. * Every element has the following properties:
  367. *
  368. * * level - problem level.
  369. * * message - A human-readable string describing the issue.
  370. * * node - A reference to the problematic node.
  371. *
  372. * The level means:
  373. * 1 - The issue was repaired (only happens if REPAIR was turned on).
  374. * 2 - A warning.
  375. * 3 - An error.
  376. *
  377. * @param int $options
  378. *
  379. * @return array
  380. */
  381. public function validate($options = 0)
  382. {
  383. $warnings = parent::validate($options);
  384. if ($ver = $this->VERSION) {
  385. if ('2.0' !== (string) $ver) {
  386. $warnings[] = [
  387. 'level' => 3,
  388. 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
  389. 'node' => $this,
  390. ];
  391. }
  392. }
  393. $uidList = [];
  394. $componentsFound = 0;
  395. $componentTypes = [];
  396. foreach ($this->children() as $child) {
  397. if ($child instanceof Component) {
  398. ++$componentsFound;
  399. if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) {
  400. continue;
  401. }
  402. $componentTypes[] = $child->name;
  403. $uid = (string) $child->UID;
  404. $isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1;
  405. if (isset($uidList[$uid])) {
  406. ++$uidList[$uid]['count'];
  407. if ($isMaster && $uidList[$uid]['hasMaster']) {
  408. $warnings[] = [
  409. 'level' => 3,
  410. 'message' => 'More than one master object was found for the object with UID '.$uid,
  411. 'node' => $this,
  412. ];
  413. }
  414. $uidList[$uid]['hasMaster'] += $isMaster;
  415. } else {
  416. $uidList[$uid] = [
  417. 'count' => 1,
  418. 'hasMaster' => $isMaster,
  419. ];
  420. }
  421. }
  422. }
  423. if (0 === $componentsFound) {
  424. $warnings[] = [
  425. 'level' => 3,
  426. 'message' => 'An iCalendar object must have at least 1 component.',
  427. 'node' => $this,
  428. ];
  429. }
  430. if ($options & self::PROFILE_CALDAV) {
  431. if (count($uidList) > 1) {
  432. $warnings[] = [
  433. 'level' => 3,
  434. 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
  435. 'node' => $this,
  436. ];
  437. }
  438. if (0 === count($componentTypes)) {
  439. $warnings[] = [
  440. 'level' => 3,
  441. 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).',
  442. 'node' => $this,
  443. ];
  444. }
  445. if (count(array_unique($componentTypes)) > 1) {
  446. $warnings[] = [
  447. 'level' => 3,
  448. 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).',
  449. 'node' => $this,
  450. ];
  451. }
  452. if (isset($this->METHOD)) {
  453. $warnings[] = [
  454. 'level' => 3,
  455. 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.',
  456. 'node' => $this,
  457. ];
  458. }
  459. }
  460. return $warnings;
  461. }
  462. /**
  463. * Returns all components with a specific UID value.
  464. *
  465. * @return array
  466. */
  467. public function getByUID($uid)
  468. {
  469. return array_filter($this->getComponents(), function ($item) use ($uid) {
  470. if (!$itemUid = $item->select('UID')) {
  471. return false;
  472. }
  473. $itemUid = current($itemUid)->getValue();
  474. return $uid === $itemUid;
  475. });
  476. }
  477. }