= $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(); 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'] = trim($m[1]); if (preg_match('/^LOCATION[^:]*:(.+)$/m', $b, $m)) $e['location'] = trim($m[1]); // Handles: DTSTART:YYYYMMDD, DTSTART:YYYYMMDDTHHmmss[Z], DTSTART;TZID=...:YYYYMMDDTHHmmss if (preg_match('/^DTSTART(?:;[^:]+)?:([\dTZ]+)/m', $b, $m)) { $raw = $m[1]; if (strlen($raw) === 8) { $e['start'] = 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['start'] = $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); }