83 lines
2.9 KiB
PHP
83 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Calendar;
|
|
|
|
use Carbon\CarbonInterface;
|
|
use DateTimeZone;
|
|
use RuntimeException;
|
|
use Sabre\VObject\Component\VCalendar;
|
|
use Sabre\VObject\Component\VEvent;
|
|
use Sabre\VObject\Reader;
|
|
|
|
class CalendarOccurrenceExpander
|
|
{
|
|
public function expand(string $ical, CarbonInterface $from, CarbonInterface $to): string
|
|
{
|
|
if (! preg_match('/^(RRULE|RDATE|RECURRENCE-ID)[;:]/mi', $ical)) {
|
|
return $ical;
|
|
}
|
|
|
|
$calendar = Reader::read($ical);
|
|
if (! $calendar instanceof VCalendar) {
|
|
throw new RuntimeException('La source agenda ne contient pas un calendrier iCalendar.');
|
|
}
|
|
|
|
$additionalDates = clone $calendar;
|
|
$additionalUids = [];
|
|
|
|
foreach ($calendar->select('VEVENT') as $event) {
|
|
// A cancelled detached occurrence may contain only UID and RECURRENCE-ID.
|
|
// It must still suppress the original occurrence during expansion.
|
|
if (! isset($event->DTSTART) && isset($event->{'RECURRENCE-ID'})) {
|
|
$start = clone $event->{'RECURRENCE-ID'};
|
|
$start->name = 'DTSTART';
|
|
$event->add($start);
|
|
}
|
|
|
|
// VObject chooses RDATE instead of RRULE when both are present.
|
|
// Expand their union separately, then deduplicate by occurrence identity.
|
|
if (isset($event->RRULE, $event->RDATE)) {
|
|
$additionalUids[] = (string) $event->UID;
|
|
unset($event->RDATE);
|
|
}
|
|
}
|
|
|
|
$timezone = new DateTimeZone((string) config('app.timezone', 'UTC'));
|
|
$expanded = $calendar->expand($from, $to, $timezone);
|
|
|
|
if ($additionalUids !== []) {
|
|
foreach ($additionalDates->select('VEVENT') as $event) {
|
|
if (! in_array((string) $event->UID, $additionalUids, true)) {
|
|
$additionalDates->remove($event);
|
|
} else {
|
|
unset($event->RRULE);
|
|
if (! isset($event->DTSTART) && isset($event->{'RECURRENCE-ID'})) {
|
|
$start = clone $event->{'RECURRENCE-ID'};
|
|
$start->name = 'DTSTART';
|
|
$event->add($start);
|
|
}
|
|
}
|
|
}
|
|
|
|
$seen = [];
|
|
foreach ($expanded->select('VEVENT') as $event) {
|
|
$seen[$this->identity($event)] = true;
|
|
}
|
|
foreach ($additionalDates->expand($from, $to, $timezone)->select('VEVENT') as $event) {
|
|
$key = $this->identity($event);
|
|
if (! isset($seen[$key])) {
|
|
$expanded->add(clone $event);
|
|
$seen[$key] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $expanded->serialize();
|
|
}
|
|
|
|
private function identity(VEvent $event): string
|
|
{
|
|
return (string) $event->UID.'|'.(string) $event->{'RECURRENCE-ID'};
|
|
}
|
|
}
|