VCalendar.php 14 KB

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