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