FreeBusyGenerator.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <?php
  2. namespace Sabre\VObject;
  3. use DateTimeZone;
  4. use Sabre\VObject\Component\VCalendar;
  5. use Sabre\VObject\Recur\EventIterator;
  6. use Sabre\VObject\Recur\NoInstancesException;
  7. /**
  8. * This class helps with generating FREEBUSY reports based on existing sets of
  9. * objects.
  10. *
  11. * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
  12. * generates a single VFREEBUSY object.
  13. *
  14. * VFREEBUSY components are described in RFC5545, The rules for what should
  15. * go in a single freebusy report is taken from RFC4791, section 7.10.
  16. *
  17. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  18. * @author Evert Pot (http://evertpot.com/)
  19. * @license http://sabre.io/license/ Modified BSD License
  20. */
  21. class FreeBusyGenerator {
  22. /**
  23. * Input objects
  24. *
  25. * @var array
  26. */
  27. protected $objects;
  28. /**
  29. * Start of range
  30. *
  31. * @var DateTime|null
  32. */
  33. protected $start;
  34. /**
  35. * End of range
  36. *
  37. * @var DateTime|null
  38. */
  39. protected $end;
  40. /**
  41. * VCALENDAR object
  42. *
  43. * @var Component
  44. */
  45. protected $baseObject;
  46. /**
  47. * Reference timezone.
  48. *
  49. * When we are calculating busy times, and we come across so-called
  50. * floating times (times without a timezone), we use the reference timezone
  51. * instead.
  52. *
  53. * This is also used for all-day events.
  54. *
  55. * This defaults to UTC.
  56. *
  57. * @var DateTimeZone
  58. */
  59. protected $timeZone;
  60. /**
  61. * Creates the generator.
  62. *
  63. * Check the setTimeRange and setObjects methods for details about the
  64. * arguments.
  65. *
  66. * @param DateTime $start
  67. * @param DateTime $end
  68. * @param mixed $objects
  69. * @param DateTimeZone $timeZone
  70. * @return void
  71. */
  72. public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null, DateTimeZone $timeZone = null) {
  73. if ($start && $end) {
  74. $this->setTimeRange($start, $end);
  75. }
  76. if ($objects) {
  77. $this->setObjects($objects);
  78. }
  79. if (is_null($timeZone)) {
  80. $timeZone = new DateTimeZone('UTC');
  81. }
  82. $this->setTimeZone($timeZone);
  83. }
  84. /**
  85. * Sets the VCALENDAR object.
  86. *
  87. * If this is set, it will not be generated for you. You are responsible
  88. * for setting things like the METHOD, CALSCALE, VERSION, etc..
  89. *
  90. * The VFREEBUSY object will be automatically added though.
  91. *
  92. * @param Component $vcalendar
  93. * @return void
  94. */
  95. public function setBaseObject(Component $vcalendar) {
  96. $this->baseObject = $vcalendar;
  97. }
  98. /**
  99. * Sets the input objects
  100. *
  101. * You must either specify a valendar object as a strong, or as the parse
  102. * Component.
  103. * It's also possible to specify multiple objects as an array.
  104. *
  105. * @param mixed $objects
  106. * @return void
  107. */
  108. public function setObjects($objects) {
  109. if (!is_array($objects)) {
  110. $objects = array($objects);
  111. }
  112. $this->objects = array();
  113. foreach($objects as $object) {
  114. if (is_string($object)) {
  115. $this->objects[] = Reader::read($object);
  116. } elseif ($object instanceof Component) {
  117. $this->objects[] = $object;
  118. } else {
  119. throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
  120. }
  121. }
  122. }
  123. /**
  124. * Sets the time range
  125. *
  126. * Any freebusy object falling outside of this time range will be ignored.
  127. *
  128. * @param DateTime $start
  129. * @param DateTime $end
  130. * @return void
  131. */
  132. public function setTimeRange(\DateTime $start = null, \DateTime $end = null) {
  133. $this->start = $start;
  134. $this->end = $end;
  135. }
  136. /**
  137. * Sets the reference timezone for floating times.
  138. *
  139. * @param DateTimeZone $timeZone
  140. * @return void
  141. */
  142. public function setTimeZone(DateTimeZone $timeZone) {
  143. $this->timeZone = $timeZone;
  144. }
  145. /**
  146. * Parses the input data and returns a correct VFREEBUSY object, wrapped in
  147. * a VCALENDAR.
  148. *
  149. * @return Component
  150. */
  151. public function getResult() {
  152. $busyTimes = array();
  153. foreach($this->objects as $key=>$object) {
  154. foreach($object->getBaseComponents() as $component) {
  155. switch($component->name) {
  156. case 'VEVENT' :
  157. $FBTYPE = 'BUSY';
  158. if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) {
  159. break;
  160. }
  161. if (isset($component->STATUS)) {
  162. $status = strtoupper($component->STATUS);
  163. if ($status==='CANCELLED') {
  164. break;
  165. }
  166. if ($status==='TENTATIVE') {
  167. $FBTYPE = 'BUSY-TENTATIVE';
  168. }
  169. }
  170. $times = array();
  171. if ($component->RRULE) {
  172. try {
  173. $iterator = new EventIterator($object, (string)$component->uid, $this->timeZone);
  174. } catch (NoInstancesException $e) {
  175. // This event is recurring, but it doesn't have a single
  176. // instance. We are skipping this event from the output
  177. // entirely.
  178. unset($this->objects[$key]);
  179. continue;
  180. }
  181. if ($this->start) {
  182. $iterator->fastForward($this->start);
  183. }
  184. $maxRecurrences = 200;
  185. while($iterator->valid() && --$maxRecurrences) {
  186. $startTime = $iterator->getDTStart();
  187. if ($this->end && $startTime > $this->end) {
  188. break;
  189. }
  190. $times[] = array(
  191. $iterator->getDTStart(),
  192. $iterator->getDTEnd(),
  193. );
  194. $iterator->next();
  195. }
  196. } else {
  197. $startTime = $component->DTSTART->getDateTime($this->timeZone);
  198. if ($this->end && $startTime > $this->end) {
  199. break;
  200. }
  201. $endTime = null;
  202. if (isset($component->DTEND)) {
  203. $endTime = $component->DTEND->getDateTime($this->timeZone);
  204. } elseif (isset($component->DURATION)) {
  205. $duration = DateTimeParser::parseDuration((string)$component->DURATION);
  206. $endTime = clone $startTime;
  207. $endTime->add($duration);
  208. } elseif (!$component->DTSTART->hasTime()) {
  209. $endTime = clone $startTime;
  210. $endTime->modify('+1 day');
  211. } else {
  212. // The event had no duration (0 seconds)
  213. break;
  214. }
  215. $times[] = array($startTime, $endTime);
  216. }
  217. foreach($times as $time) {
  218. if ($this->end && $time[0] > $this->end) break;
  219. if ($this->start && $time[1] < $this->start) break;
  220. $busyTimes[] = array(
  221. $time[0],
  222. $time[1],
  223. $FBTYPE,
  224. );
  225. }
  226. break;
  227. case 'VFREEBUSY' :
  228. foreach($component->FREEBUSY as $freebusy) {
  229. $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY';
  230. // Skipping intervals marked as 'free'
  231. if ($fbType==='FREE')
  232. continue;
  233. $values = explode(',', $freebusy);
  234. foreach($values as $value) {
  235. list($startTime, $endTime) = explode('/', $value);
  236. $startTime = DateTimeParser::parseDateTime($startTime);
  237. if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') {
  238. $duration = DateTimeParser::parseDuration($endTime);
  239. $endTime = clone $startTime;
  240. $endTime->add($duration);
  241. } else {
  242. $endTime = DateTimeParser::parseDateTime($endTime);
  243. }
  244. if($this->start && $this->start > $endTime) continue;
  245. if($this->end && $this->end < $startTime) continue;
  246. $busyTimes[] = array(
  247. $startTime,
  248. $endTime,
  249. $fbType
  250. );
  251. }
  252. }
  253. break;
  254. }
  255. }
  256. }
  257. if ($this->baseObject) {
  258. $calendar = $this->baseObject;
  259. } else {
  260. $calendar = new VCalendar();
  261. }
  262. $vfreebusy = $calendar->createComponent('VFREEBUSY');
  263. $calendar->add($vfreebusy);
  264. if ($this->start) {
  265. $dtstart = $calendar->createProperty('DTSTART');
  266. $dtstart->setDateTime($this->start);
  267. $vfreebusy->add($dtstart);
  268. }
  269. if ($this->end) {
  270. $dtend = $calendar->createProperty('DTEND');
  271. $dtend->setDateTime($this->end);
  272. $vfreebusy->add($dtend);
  273. }
  274. $dtstamp = $calendar->createProperty('DTSTAMP');
  275. $dtstamp->setDateTime(new \DateTime('now', new \DateTimeZone('UTC')));
  276. $vfreebusy->add($dtstamp);
  277. foreach($busyTimes as $busyTime) {
  278. $busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
  279. $busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
  280. $prop = $calendar->createProperty(
  281. 'FREEBUSY',
  282. $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
  283. );
  284. $prop['FBTYPE'] = $busyTime[2];
  285. $vfreebusy->add($prop);
  286. }
  287. return $calendar;
  288. }
  289. }