diff --git a/js/ct-events.js b/js/ct-events.js index e013b44..7f60d1e 100644 --- a/js/ct-events.js +++ b/js/ct-events.js @@ -89,7 +89,7 @@ function display_event_date(item) { return m("span.timerange", display_date_utc(start, utc_start_date), " - ", display_date_utc(end_day, utc_end_day_date)); } } - + if (start_date === end_date) { return m("span.timerange", display_datetime(start, `${start_date}, ${start_time}`), " - ", display_datetime(end, end_time)); } else { @@ -102,31 +102,43 @@ function display_event_date(item) { } } -function escapeICSField(text) { - return text.replace(/\\/g, "\\\\") - .replace(/[,;]/g, "\\$&") - .replace(/\n/g, "\\n"); +function format_ICS_text(text) { + return text.replace(/[\\,;]/g, "\\$&") // escape backslash and separators + .replace(/\n/g, "\\n") // escape newline + .replace(/[\x00-\x1f]+/g, " ") // replace control characters with whitespace + ; +} + +function format_ICS_date(date) { + if (date) { + date = new Date(date); + } else { + date = new Date(); // default to "now" + } + // remove "-" and ":" separators (but keep "T"), and drop microseconds + return date.toISOString().replace(/-|:|\.\d+/g, ""); } function createICSLink(item, settings) { - const start = new Date(item.calculated.startDate).toISOString().replace(/-|:|\.\d+/g, ''); - const end = new Date(item.calculated.endDate).toISOString().replace(/-|:|\.\d+/g, ''); + const start = format_ICS_date(item.calculated.startDate); + const end = format_ICS_date(item.calculated.endDate); const summary = item.base.caption; const description = item.base.information || ""; const location = item.base.address?.meetingAt || ""; // Escape special characters for ICS fields - const escapedSummary = escapeICSField(summary /* .replace(/['"]/g, "") */); - const escapedDescription = escapeICSField(description /* .replace(/['"]/g, "") */); - const escapedLocation = escapeICSField(location); - + const escapedSummary = format_ICS_text(summary /* .replace(/['"]/g, "") */); + const escapedDescription = format_ICS_text(description /* .replace(/['"]/g, "") */); + const escapedLocation = format_ICS_text(location); + const icsContent = `BEGIN:VCALENDAR VERSION:2.0 -PRODID:-//Evang Kirchengemeinde Malmsheim//Gottesdienste//EN +PRODID:-//churchtools-next-events//EN BEGIN:VEVENT -UID:${crypto.randomUUID()}@malmsheim-evangelisch.de -DTSTAMP:${new Date().toISOString().replace(/-|:|\.\d+/g, '')} +UID:appointment-${item.base.id}@${window.location.hostname} +CREATED:${format_ICS_date(item.base.meta?.createdDate)} +LAST-MODIFIED:${format_ICS_date(item.base.meta?.modifiedDate)} DTSTART:${start} DTEND:${end} SUMMARY:${escapedSummary} @@ -273,7 +285,7 @@ var AppointmentLocation = { if (!location) { return m("["); } - + return m("p", [ fa_solid_icon(settings, "location-dot"), " ", @@ -293,7 +305,7 @@ var AppointmentLink = { if (!url.startsWith("http")) { url = "https://" + url; } - + return m("p", [ fa_solid_icon(settings, "link"), " ", @@ -389,24 +401,30 @@ async function load_events(dom_node, defaults) { credentials: "omit", mode: "cors", } - + const r_cal_regex = new RegExp(settings.calendar_regex, "i"); const r_event_regex = new RegExp(settings.event_regex, "i"); const all_calendars = (await (await fetch(settings.instance + "/api/calendars", req_config)).json()).data; const calendars = all_calendars.filter((calendar) => r_cal_regex.exec(calendar.name)); const calendar_ids = calendars.map((calendar) => calendar.id); - const filters = calendar_ids.map((cal_id) => `calendar_ids[]=${cal_id}`); - const from = new Date(); - filters.push(`from=${format_date(from)}`); - const to = new Date(from.valueOf() + settings.days * 86400 * 1000); - filters.push(`to=${format_date(to)}`); - const query = filters.join("&"); - const all_appointments = (await (await fetch(settings.instance + `/api/calendars/appointments?${query}`, req_config)).json()).data; - const appointments = all_appointments.filter((appointment) => r_event_regex.exec(appointment.base.title)); - - if (appointments.length > settings.limit) { - appointments.length = settings.limit; + let appointments; + + if (calendar_ids.length) { + const filters = calendar_ids.map((cal_id) => `calendar_ids[]=${cal_id}`); + const from = new Date(); + filters.push(`from=${format_date(from)}`); + const to = new Date(from.valueOf() + settings.days * 86400 * 1000); + filters.push(`to=${format_date(to)}`); + const query = filters.join("&"); + const all_appointments = (await (await fetch(settings.instance + `/api/calendars/appointments?${query}`, req_config)).json()).data; + appointments = all_appointments.filter((appointment) => r_event_regex.exec(appointment.base.title)); + + if (appointments.length > settings.limit) { + appointments.length = settings.limit; + } + } else { + appointments = []; } const Appointments = {