0) { $event = $this->events->find($eventId); if ($event !== null && \ProCalendarWP\Events\EventVisibility::isPublic($eventId)) { $items[] = $event; } } if ($items === [] && ! $feed) { status_header(404); exit; } nocache_headers(); header('Content-Type: text/calendar; charset=utf-8'); header('Content-Disposition: attachment; filename="procalendarwp-events.ics"'); if ($feed) { echo "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//ProEventsWP//Events//EN\r\nCALSCALE:GREGORIAN\r\n"; $offset = 0; $emitted = []; $category = sanitize_title((string) get_query_var('pcwp_ics_category')); do { $batch = $this->events->occurrences(['start' => gmdate(DATE_ATOM), 'limit' => 100, 'offset' => $offset, 'category' => $category]); foreach ($batch as $occurrence) { if (isset($emitted[$occurrence->eventId])) { continue; } $emitted[$occurrence->eventId] = true; $event = $this->events->find($occurrence->eventId); if ($event !== null && \ProCalendarWP\Events\EventVisibility::isPublic($event->id)) { echo $this->calendar([$event], false); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } \ProCalendarWP\Events\WpEventRepository::clearEventCache($occurrence->eventId); } $offset += count($batch); if (function_exists('wp_cache_flush_runtime') && wp_cache_supports('flush_runtime')) { wp_cache_flush_runtime(); } } while (count($batch) === 100); echo "END:VCALENDAR\r\n"; } else { echo $this->calendar($items); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } exit; } /** @param list $events */ private function calendar(array $events, bool $envelope = true): string { $lines = $envelope ? ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//ProEventsWP//Events//EN', 'CALSCALE:GREGORIAN'] : []; foreach ($events as $event) { $slots = ($event->schedule['type'] ?? '') === 'daily' ? \ProCalendarWP\Events\DailySchedule::intervals($event->schedule, $event->timezone) : [['id' => '', 'time' => new \ProCalendarWP\Events\EventTime($event->startLocal, $event->endLocal, $event->timezone)]]; foreach ($slots as $slot) { $start = $slot['time']->startUtc(); $end = $slot['time']->endUtc(); $lines[] = 'BEGIN:VEVENT'; $lines[] = 'UID:event-' . $event->id . ($slot['id'] !== '' ? '-slot-' . $slot['id'] : '') . '@' . wp_parse_url(home_url(), PHP_URL_HOST); $lines[] = 'DTSTAMP:' . gmdate('Ymd\THis\Z'); if ($event->allDay) { $lines[] = 'DTSTART;VALUE=DATE:' . $event->startLocal->format('Ymd'); $lines[] = 'DTEND;VALUE=DATE:' . $event->endLocal->modify('+1 day')->format('Ymd'); } else { $lines[] = 'DTSTART:' . $start->format('Ymd\THis\Z'); $lines[] = 'DTEND:' . $end->format('Ymd\THis\Z'); } $lines[] = 'SUMMARY:' . $this->escape($event->title); $lines[] = 'DESCRIPTION:' . $this->escape(wp_strip_all_tags($event->excerpt !== '' ? $event->excerpt : $event->description)); $lines[] = 'URL:' . $this->escape((string) get_permalink($event->id)); $lines[] = 'STATUS:' . ($event->status === 'canceled' ? 'CANCELLED' : (in_array($event->status, ['scheduled', 'rescheduled'], true) ? 'CONFIRMED' : 'TENTATIVE')); $lines[] = 'END:VEVENT'; } } if ($envelope) { $lines[] = 'END:VCALENDAR'; } return implode("\r\n", array_map([$this, 'fold'], $lines)) . "\r\n"; } private function escape(string $value): string { return str_replace(["\\", ";", ",", "\r\n", "\r", "\n"], ["\\\\", '\\;', '\\,', '\\n', '\\n', '\\n'], $value); } private function fold(string $line): string { $parts = str_split($line, 73); return implode("\r\n ", $parts); } }