PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.20
Booking for Appointments and Events Calendar – Amelia v1.2.20
2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / vendor / sabre / vobject / lib / FreeBusyGenerator.php
ameliabooking / vendor / sabre / vobject / lib Last commit date
Component 1 year ago ITip 1 year ago Parser 1 year ago Property 1 year ago Recur 1 year ago Splitter 1 year ago TimezoneGuesser 1 year ago timezonedata 1 year ago BirthdayCalendarGenerator.php 1 year ago Cli.php 1 year ago Component.php 1 year ago DateTimeParser.php 1 year ago Document.php 1 year ago ElementList.php 1 year ago EofException.php 1 year ago FreeBusyData.php 1 year ago FreeBusyGenerator.php 1 year ago InvalidDataException.php 1 year ago Node.php 1 year ago PHPUnitAssertions.php 1 year ago Parameter.php 1 year ago ParseException.php 1 year ago Property.php 1 year ago Reader.php 1 year ago Settings.php 1 year ago StringUtil.php 1 year ago TimeZoneUtil.php 1 year ago UUIDUtil.php 1 year ago VCardConverter.php 1 year ago Version.php 1 year ago Writer.php 1 year ago
FreeBusyGenerator.php
550 lines
1 <?php
2
3 namespace Sabre\VObject;
4
5 use DateTimeImmutable;
6 use DateTimeInterface;
7 use DateTimeZone;
8 use Sabre\VObject\Component\VCalendar;
9 use Sabre\VObject\Recur\EventIterator;
10 use Sabre\VObject\Recur\NoInstancesException;
11
12 /**
13 * This class helps with generating FREEBUSY reports based on existing sets of
14 * objects.
15 *
16 * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
17 * generates a single VFREEBUSY object.
18 *
19 * VFREEBUSY components are described in RFC5545, The rules for what should
20 * go in a single freebusy report is taken from RFC4791, section 7.10.
21 *
22 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
23 * @author Evert Pot (http://evertpot.com/)
24 * @license http://sabre.io/license/ Modified BSD License
25 */
26 class FreeBusyGenerator
27 {
28 /**
29 * Input objects.
30 *
31 * @var array
32 */
33 protected $objects = [];
34
35 /**
36 * Start of range.
37 *
38 * @var DateTimeInterface|null
39 */
40 protected $start;
41
42 /**
43 * End of range.
44 *
45 * @var DateTimeInterface|null
46 */
47 protected $end;
48
49 /**
50 * VCALENDAR object.
51 *
52 * @var Document
53 */
54 protected $baseObject;
55
56 /**
57 * Reference timezone.
58 *
59 * When we are calculating busy times, and we come across so-called
60 * floating times (times without a timezone), we use the reference timezone
61 * instead.
62 *
63 * This is also used for all-day events.
64 *
65 * This defaults to UTC.
66 *
67 * @var DateTimeZone
68 */
69 protected $timeZone;
70
71 /**
72 * A VAVAILABILITY document.
73 *
74 * If this is set, its information will be included when calculating
75 * freebusy time.
76 *
77 * @var Document
78 */
79 protected $vavailability;
80
81 /**
82 * Creates the generator.
83 *
84 * Check the setTimeRange and setObjects methods for details about the
85 * arguments.
86 *
87 * @param DateTimeInterface $start
88 * @param DateTimeInterface $end
89 * @param mixed $objects
90 * @param DateTimeZone $timeZone
91 */
92 public function __construct(?DateTimeInterface $start = null, ?DateTimeInterface $end = null, $objects = null, ?DateTimeZone $timeZone = null)
93 {
94 $this->setTimeRange($start, $end);
95
96 if ($objects) {
97 $this->setObjects($objects);
98 }
99 if (is_null($timeZone)) {
100 $timeZone = new DateTimeZone('UTC');
101 }
102 $this->setTimeZone($timeZone);
103 }
104
105 /**
106 * Sets the VCALENDAR object.
107 *
108 * If this is set, it will not be generated for you. You are responsible
109 * for setting things like the METHOD, CALSCALE, VERSION, etc..
110 *
111 * The VFREEBUSY object will be automatically added though.
112 */
113 public function setBaseObject(Document $vcalendar)
114 {
115 $this->baseObject = $vcalendar;
116 }
117
118 /**
119 * Sets a VAVAILABILITY document.
120 */
121 public function setVAvailability(Document $vcalendar)
122 {
123 $this->vavailability = $vcalendar;
124 }
125
126 /**
127 * Sets the input objects.
128 *
129 * You must either specify a vcalendar object as a string, or as the parse
130 * Component.
131 * It's also possible to specify multiple objects as an array.
132 *
133 * @param mixed $objects
134 */
135 public function setObjects($objects)
136 {
137 if (!is_array($objects)) {
138 $objects = [$objects];
139 }
140
141 $this->objects = [];
142 foreach ($objects as $object) {
143 if (is_string($object) || is_resource($object)) {
144 $this->objects[] = Reader::read($object);
145 } elseif ($object instanceof Component) {
146 $this->objects[] = $object;
147 } else {
148 throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
149 }
150 }
151 }
152
153 /**
154 * Sets the time range.
155 *
156 * Any freebusy object falling outside of this time range will be ignored.
157 *
158 * @param DateTimeInterface $start
159 * @param DateTimeInterface $end
160 */
161 public function setTimeRange(?DateTimeInterface $start = null, ?DateTimeInterface $end = null)
162 {
163 if (!$start) {
164 $start = new DateTimeImmutable(Settings::$minDate);
165 }
166 if (!$end) {
167 $end = new DateTimeImmutable(Settings::$maxDate);
168 }
169 $this->start = $start;
170 $this->end = $end;
171 }
172
173 /**
174 * Sets the reference timezone for floating times.
175 */
176 public function setTimeZone(DateTimeZone $timeZone)
177 {
178 $this->timeZone = $timeZone;
179 }
180
181 /**
182 * Parses the input data and returns a correct VFREEBUSY object, wrapped in
183 * a VCALENDAR.
184 *
185 * @return Component
186 */
187 public function getResult()
188 {
189 $fbData = new FreeBusyData(
190 $this->start->getTimeStamp(),
191 $this->end->getTimeStamp()
192 );
193 if ($this->vavailability) {
194 $this->calculateAvailability($fbData, $this->vavailability);
195 }
196
197 $this->calculateBusy($fbData, $this->objects);
198
199 return $this->generateFreeBusyCalendar($fbData);
200 }
201
202 /**
203 * This method takes a VAVAILABILITY component and figures out all the
204 * available times.
205 */
206 protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability)
207 {
208 $vavailComps = iterator_to_array($vavailability->VAVAILABILITY);
209 usort(
210 $vavailComps,
211 function ($a, $b) {
212 // We need to order the components by priority. Priority 1
213 // comes first, up until priority 9. Priority 0 comes after
214 // priority 9. No priority implies priority 0.
215 //
216 // Yes, I'm serious.
217 $priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0;
218 $priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0;
219
220 if (0 === $priorityA) {
221 $priorityA = 10;
222 }
223 if (0 === $priorityB) {
224 $priorityB = 10;
225 }
226
227 return $priorityA - $priorityB;
228 }
229 );
230
231 // Now we go over all the VAVAILABILITY components and figure if
232 // there's any we don't need to consider.
233 //
234 // This is can be because of one of two reasons: either the
235 // VAVAILABILITY component falls outside the time we are interested in,
236 // or a different VAVAILABILITY component with a higher priority has
237 // already completely covered the time-range.
238 $old = $vavailComps;
239 $new = [];
240
241 foreach ($old as $vavail) {
242 list($compStart, $compEnd) = $vavail->getEffectiveStartEnd();
243
244 // We don't care about datetimes that are earlier or later than the
245 // start and end of the freebusy report, so this gets normalized
246 // first.
247 if (is_null($compStart) || $compStart < $this->start) {
248 $compStart = $this->start;
249 }
250 if (is_null($compEnd) || $compEnd > $this->end) {
251 $compEnd = $this->end;
252 }
253
254 // If the item fell out of the timerange, we can just skip it.
255 if ($compStart > $this->end || $compEnd < $this->start) {
256 continue;
257 }
258
259 // Going through our existing list of components to see if there's
260 // a higher priority component that already fully covers this one.
261 foreach ($new as $higherVavail) {
262 list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd();
263 if (
264 (is_null($higherStart) || $higherStart < $compStart) &&
265 (is_null($higherEnd) || $higherEnd > $compEnd)
266 ) {
267 // Component is fully covered by a higher priority
268 // component. We can skip this component.
269 continue 2;
270 }
271 }
272
273 // We're keeping it!
274 $new[] = $vavail;
275 }
276
277 // Lastly, we need to traverse the remaining components and fill in the
278 // freebusydata slots.
279 //
280 // We traverse the components in reverse, because we want the higher
281 // priority components to override the lower ones.
282 foreach (array_reverse($new) as $vavail) {
283 $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE';
284 list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd();
285
286 // Making the component size no larger than the requested free-busy
287 // report range.
288 if (!$vavailStart || $vavailStart < $this->start) {
289 $vavailStart = $this->start;
290 }
291 if (!$vavailEnd || $vavailEnd > $this->end) {
292 $vavailEnd = $this->end;
293 }
294
295 // Marking the entire time range of the VAVAILABILITY component as
296 // busy.
297 $fbData->add(
298 $vavailStart->getTimeStamp(),
299 $vavailEnd->getTimeStamp(),
300 $busyType
301 );
302
303 // Looping over the AVAILABLE components.
304 if (isset($vavail->AVAILABLE)) {
305 foreach ($vavail->AVAILABLE as $available) {
306 list($availStart, $availEnd) = $available->getEffectiveStartEnd();
307 $fbData->add(
308 $availStart->getTimeStamp(),
309 $availEnd->getTimeStamp(),
310 'FREE'
311 );
312
313 if ($available->RRULE) {
314 // Our favourite thing: recurrence!!
315
316 $rruleIterator = new Recur\RRuleIterator(
317 $available->RRULE->getValue(),
318 $availStart
319 );
320 $rruleIterator->fastForward($vavailStart);
321
322 $startEndDiff = $availStart->diff($availEnd);
323
324 while ($rruleIterator->valid()) {
325 $recurStart = $rruleIterator->current();
326 $recurEnd = $recurStart->add($startEndDiff);
327
328 if ($recurStart > $vavailEnd) {
329 // We're beyond the legal timerange.
330 break;
331 }
332
333 if ($recurEnd > $vavailEnd) {
334 // Truncating the end if it exceeds the
335 // VAVAILABILITY end.
336 $recurEnd = $vavailEnd;
337 }
338
339 $fbData->add(
340 $recurStart->getTimeStamp(),
341 $recurEnd->getTimeStamp(),
342 'FREE'
343 );
344
345 $rruleIterator->next();
346 }
347 }
348 }
349 }
350 }
351 }
352
353 /**
354 * This method takes an array of iCalendar objects and applies its busy
355 * times on fbData.
356 *
357 * @param VCalendar[] $objects
358 */
359 protected function calculateBusy(FreeBusyData $fbData, array $objects)
360 {
361 foreach ($objects as $key => $object) {
362 foreach ($object->getBaseComponents() as $component) {
363 switch ($component->name) {
364 case 'VEVENT':
365 $FBTYPE = 'BUSY';
366 if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) {
367 break;
368 }
369 if (isset($component->STATUS)) {
370 $status = strtoupper($component->STATUS);
371 if ('CANCELLED' === $status) {
372 break;
373 }
374 if ('TENTATIVE' === $status) {
375 $FBTYPE = 'BUSY-TENTATIVE';
376 }
377 }
378
379 $times = [];
380
381 if ($component->RRULE) {
382 try {
383 $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone);
384 } catch (NoInstancesException $e) {
385 // This event is recurring, but it doesn't have a single
386 // instance. We are skipping this event from the output
387 // entirely.
388 unset($this->objects[$key]);
389 break;
390 }
391
392 if ($this->start) {
393 $iterator->fastForward($this->start);
394 }
395
396 $maxRecurrences = Settings::$maxRecurrences;
397
398 while ($iterator->valid() && --$maxRecurrences) {
399 $startTime = $iterator->getDTStart();
400 if ($this->end && $startTime > $this->end) {
401 break;
402 }
403 $times[] = [
404 $iterator->getDTStart(),
405 $iterator->getDTEnd(),
406 ];
407
408 $iterator->next();
409 }
410 } else {
411 $startTime = $component->DTSTART->getDateTime($this->timeZone);
412 if ($this->end && $startTime > $this->end) {
413 break;
414 }
415 $endTime = null;
416 if (isset($component->DTEND)) {
417 $endTime = $component->DTEND->getDateTime($this->timeZone);
418 } elseif (isset($component->DURATION)) {
419 $duration = DateTimeParser::parseDuration((string) $component->DURATION);
420 $endTime = clone $startTime;
421 $endTime = $endTime->add($duration);
422 } elseif (!$component->DTSTART->hasTime()) {
423 $endTime = clone $startTime;
424 $endTime = $endTime->modify('+1 day');
425 } else {
426 // The event had no duration (0 seconds)
427 break;
428 }
429
430 $times[] = [$startTime, $endTime];
431 }
432
433 foreach ($times as $time) {
434 if ($this->end && $time[0] > $this->end) {
435 break;
436 }
437 if ($this->start && $time[1] < $this->start) {
438 break;
439 }
440
441 $fbData->add(
442 $time[0]->getTimeStamp(),
443 $time[1]->getTimeStamp(),
444 $FBTYPE
445 );
446 }
447 break;
448
449 case 'VFREEBUSY':
450 foreach ($component->FREEBUSY as $freebusy) {
451 $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY';
452
453 // Skipping intervals marked as 'free'
454 if ('FREE' === $fbType) {
455 continue;
456 }
457
458 $values = explode(',', $freebusy);
459 foreach ($values as $value) {
460 list($startTime, $endTime) = explode('/', $value);
461 $startTime = DateTimeParser::parseDateTime($startTime);
462
463 if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) {
464 $duration = DateTimeParser::parseDuration($endTime);
465 $endTime = clone $startTime;
466 $endTime = $endTime->add($duration);
467 } else {
468 $endTime = DateTimeParser::parseDateTime($endTime);
469 }
470
471 if ($this->start && $this->start > $endTime) {
472 continue;
473 }
474 if ($this->end && $this->end < $startTime) {
475 continue;
476 }
477 $fbData->add(
478 $startTime->getTimeStamp(),
479 $endTime->getTimeStamp(),
480 $fbType
481 );
482 }
483 }
484 break;
485 }
486 }
487 }
488 }
489
490 /**
491 * This method takes a FreeBusyData object and generates the VCALENDAR
492 * object associated with it.
493 *
494 * @return VCalendar
495 */
496 protected function generateFreeBusyCalendar(FreeBusyData $fbData)
497 {
498 if ($this->baseObject) {
499 $calendar = $this->baseObject;
500 } else {
501 $calendar = new VCalendar();
502 }
503
504 $vfreebusy = $calendar->createComponent('VFREEBUSY');
505 $calendar->add($vfreebusy);
506
507 if ($this->start) {
508 $dtstart = $calendar->createProperty('DTSTART');
509 $dtstart->setDateTime($this->start);
510 $vfreebusy->add($dtstart);
511 }
512 if ($this->end) {
513 $dtend = $calendar->createProperty('DTEND');
514 $dtend->setDateTime($this->end);
515 $vfreebusy->add($dtend);
516 }
517
518 $tz = new \DateTimeZone('UTC');
519 $dtstamp = $calendar->createProperty('DTSTAMP');
520 $dtstamp->setDateTime(new DateTimeImmutable('now', $tz));
521 $vfreebusy->add($dtstamp);
522
523 foreach ($fbData->getData() as $busyTime) {
524 $busyType = strtoupper($busyTime['type']);
525
526 // Ignoring all the FREE parts, because those are already assumed.
527 if ('FREE' === $busyType) {
528 continue;
529 }
530
531 $busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz);
532 $busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz);
533
534 $prop = $calendar->createProperty(
535 'FREEBUSY',
536 $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z')
537 );
538
539 // Only setting FBTYPE if it's not BUSY, because BUSY is the
540 // default anyway.
541 if ('BUSY' !== $busyType) {
542 $prop['FBTYPE'] = $busyType;
543 }
544 $vfreebusy->add($prop);
545 }
546
547 return $calendar;
548 }
549 }
550