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