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