VCalendar.php 18 KB

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