64) { feedNotFound(); } $feed_key = escapeSql($_GET['key']); $sql = mysqli_query( $mysqli, "SELECT * FROM calendars WHERE calendar_feed_key = '$feed_key' AND calendar_feed_key IS NOT NULL AND calendar_archived_at IS NULL LIMIT 1" ); if (mysqli_num_rows($sql) !== 1) { feedNotFound(); } $calendar = mysqli_fetch_assoc($sql); $calendar_id = intval($calendar['calendar_id']); // Gather events for this calendar only $window_start = date('Y-m-d H:i:s', strtotime('-' . FEED_MONTHS_PAST . ' months')); $window_end = date('Y-m-d H:i:s', strtotime('+' . FEED_MONTHS_FUTURE . ' months')); $events_sql = mysqli_query( $mysqli, "SELECT * FROM calendar_events WHERE event_calendar_id = $calendar_id AND event_archived_at IS NULL AND ( (event_repeat IS NOT NULL AND event_repeat != '') OR event_start BETWEEN '$window_start' AND '$window_end' ) ORDER BY event_start ASC" ); $events = []; while ($row = mysqli_fetch_assoc($events_sql)) { $events[] = $row; } $ics = buildCalendarFeedIcs($calendar, $events, $config_timezone, $config_base_url); // Let clients that send If-None-Match skip the transfer entirely $etag = '"' . md5($ics) . '"'; if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) { header("HTTP/1.1 304 Not Modified"); header("ETag: $etag"); exit(); } // Record that something is actually subscribed, without turning a hammered URL // into one write per request mysqli_query( $mysqli, "UPDATE calendars SET calendar_feed_accessed_at = NOW() WHERE calendar_id = $calendar_id AND (calendar_feed_accessed_at IS NULL OR calendar_feed_accessed_at < DATE_SUB(NOW(), INTERVAL 5 MINUTE))" ); $filename = preg_replace('/[^A-Za-z0-9_-]/', '_', $calendar['calendar_name']) . '.ics'; header("Content-Type: text/calendar; charset=utf-8"); header("Content-Disposition: inline; filename=\"$filename\""); // No explicit Content-Length - mod_deflate or nginx gzip would compress the body // after this point and leave the declared length wrong, which some clients treat // as a truncated response header("Cache-Control: private, max-age=300"); header("ETag: $etag"); echo $ics;