firstOrFail(); $calendar = $instance->calendar()->with(['events.meta'])->firstOrFail(); $timezone = $instance->timezone ?? 'UTC'; $ical = $this->generateICalendarFeed($calendar->events, $timezone); return Response::make($ical, 200, [ 'Content-Type' => 'text/calendar; charset=utf-8', 'Content-Disposition' => 'inline; filename="' . $calendarUri . '.ics"', ]); } protected function generateICalendarFeed($events, string $tz): string { $output = []; $output[] = 'BEGIN:VCALENDAR'; $output[] = 'VERSION:2.0'; $output[] = 'PRODID:-//Kithkin Calendar//EN'; $output[] = 'CALSCALE:GREGORIAN'; $output[] = 'METHOD:PUBLISH'; foreach ($events as $event) { $meta = $event->meta; if (!$meta || !$meta->start_at || !$meta->end_at) { continue; } $start = Carbon::parse($meta->start_at)->timezone($tz)->format('Ymd\THis'); $end = Carbon::parse($meta->end_at)->timezone($tz)->format('Ymd\THis'); $output[] = 'BEGIN:VEVENT'; $output[] = 'UID:' . $event->uid; $output[] = 'SUMMARY:' . $this->escape($meta->title ?? '(Untitled)'); $output[] = 'DESCRIPTION:' . $this->escape($meta->description ?? ''); $output[] = 'DTSTART;TZID=' . $tz . ':' . $start; $output[] = 'DTEND;TZID=' . $tz . ':' . $end; $output[] = 'DTSTAMP:' . Carbon::parse($event->lastmodified)->format('Ymd\THis\Z'); if ($meta->location) { $output[] = 'LOCATION:' . $this->escape($meta->location); } $output[] = 'END:VEVENT'; } $output[] = 'END:VCALENDAR'; return implode("\r\n", $output); } protected function escape(?string $text): string { return str_replace(['\\', ';', ',', "\n"], ['\\\\', '\;', '\,', '\n'], $text ?? ''); } }