xref: /plugin/davcal/vendor/sabre/vobject/lib/FreeBusyGenerator.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
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