= $ttl) { $data = @file_get_contents($url); if ($data !== false) file_put_contents($cache, $data); } if (!file_exists($cache)) return []; return _ical_upcoming(file_get_contents($cache), $n); } function _ical_upcoming(string $ical, int $n): array { $events = []; $now = time(); // Unfold continuation lines before parsing $ical = preg_replace('/\r?\n[ \t]/', '', $ical); preg_match_all('/BEGIN:VEVENT(.+?)END:VEVENT/s', $ical, $blocks); foreach ($blocks[1] as $b) { $e = []; if (preg_match('/^SUMMARY[^:]*:(.+)$/m', $b, $m)) $e['title'] = _ical_unescape(trim($m[1])); if (preg_match('/^LOCATION[^:]*:(.+)$/m', $b, $m)) $e['location'] = _ical_unescape(trim($m[1])); if (preg_match('/^DESCRIPTION[^:]*:(.+)$/m', $b, $m)) $e['description'] = _ical_unescape(trim($m[1])); if (preg_match('/^URL[^:]*:(.+)$/m', $b, $m)) $e['url'] = trim($m[1]); // Handles: DTSTART:YYYYMMDD, DTSTART:YYYYMMDDTHHmmss[Z], DTSTART;TZID=...:YYYYMMDDTHHmmss foreach (['start' => 'DTSTART', 'end' => 'DTEND'] as $key => $prop) { if (preg_match('/^' . $prop . '(?:;[^:]+)?:([\dTZ]+)/m', $b, $m)) { $raw = $m[1]; if (strlen($raw) === 8) { $e[$key] = mktime(0, 0, 0, (int)substr($raw, 4, 2), (int)substr($raw, 6, 2), (int)substr($raw, 0, 4)); } else { $dt = DateTime::createFromFormat('Ymd\THis\Z', $raw, new DateTimeZone('UTC')) ?: DateTime::createFromFormat('Ymd\THis', $raw, new DateTimeZone('Europe/Paris')); if ($dt) $e[$key] = $dt->getTimestamp(); } } } if (isset($e['start'], $e['title']) && $e['start'] >= $now) $events[] = $e; } usort($events, fn($a, $b) => $a['start'] - $b['start']); return array_slice($events, 0, $n); } function _ical_unescape(string $s): string { return str_replace(['\\n', '\\,', '\\;', '\\\\'], ["\n", ',', ';', '\\'], $s); }