- fix escaping in ICS generation

- add CRLF line endings in ICS
- add line folding in ICS
- fix downloadICS()
This commit is contained in:
stefan.tollkuehn committed 2026-10-22 17:09:16 +02:00
1 parent 86caf9a3a4
commit ce80e52923
1 file changed
+109 -32
+109 -32
View File
@@ -889,6 +889,66 @@ function createRoomName(meetingName) {
* ============================================================ * ============================================================
*/ */
const ICS_LINE_LIMIT = 75;
/*
* UTF-8 byte length of a string.
*/
function byteLength(text) {
let length = 0;
for (const char of text) {
const code = char.codePointAt(0);
length += code <= 0x7f ? 1 : code <= 0x7ff ? 2 : code <= 0xffff ? 3 : 4;
}
return length;
}
/*
* Escape a text value per RFC 5545: backslash first,
* then semicolon and comma, newlines as literal "\n".
*/
function escapeICS(text) {
return text
.replace(/\\/g, "\\\\")
.replace(/;/g, "\\;")
.replace(/,/g, "\\,")
.replace(/\r?\n/g, "\\n");
}
/*
* Fold lines longer than 75 octets into CRLF + space
* continuations (RFC 5545).
*/
function foldICS(content) {
return content
.split("\r\n")
.map((line) => {
if (byteLength(line) <= ICS_LINE_LIMIT) {
return line;
}
const parts = [];
let current = "";
for (const char of line) {
if (byteLength(current + char) > ICS_LINE_LIMIT) {
parts.push(current);
current = " " + char;
} else {
current += char;
}
}
parts.push(current);
return parts.join("\r\n");
})
.join("\r\n");
}
function generateICS(meetingName, description, startTime, endTime, organizer) { function generateICS(meetingName, description, startTime, endTime, organizer) {
/* Generate a unique room name from the meeting name */ /* Generate a unique room name from the meeting name */
const roomName = createRoomName(meetingName); const roomName = createRoomName(meetingName);
@@ -911,32 +971,39 @@ function generateICS(meetingName, description, startTime, endTime, organizer) {
const dtStart = formatICSDate(startTime); const dtStart = formatICSDate(startTime);
const dtEnd = formatICSDate(endTime); const dtEnd = formatICSDate(endTime);
/* Build ICS content */ /*
const icsContent = `BEGIN:VCALENDAR * One property per line, CRLF endings, folded at
VERSION:2.0 * 75 octets (RFC 5545).
PRODID:-//BNVB//Jitsi Meeting//EN */
CALSCALE:GREGORIAN const lines = [
METHOD:PUBLISH "BEGIN:VCALENDAR",
BEGIN:VEVENT "VERSION:2.0",
UID:${uid} `PRODID:-//Jitsi Meeting Launcher//NONSGML (${CONFIG.jitsiLauncher})//EN`,
DTSTAMP:${dtStamp} "CALSCALE:GREGORIAN",
DTSTART:${dtStart} "METHOD:PUBLISH",
DTEND:${dtEnd} "BEGIN:VEVENT",
SUMMARY:${meetingName.replace(/,/g, '\\,')} `UID:${uid}`,
DESCRIPTION:${description.replace(/,/g, '\\,').replace(/\n/g, '\\n')} `DTSTAMP:${dtStamp}`,
LOCATION:${meetingUrl} `DTSTART:${dtStart}`,
ORGANIZER:MAILTO:${organizer} `DTEND:${dtEnd}`,
STATUS:CONFIRMED `SUMMARY:${escapeICS(meetingName)}`,
URL:${meetingUrl} `DESCRIPTION:${escapeICS(description)}`,
SEQUENCE:0 `LOCATION:${meetingUrl}`,
TRANSP:OPAQUE `ORGANIZER:MAILTO:${organizer}`,
BEGIN:VALARM "STATUS:CONFIRMED",
ACTION:DISPLAY `URL:${meetingUrl}`,
DESCRIPTION:Reminder "SEQUENCE:0",
TRIGGER:-PT15M "TRANSP:OPAQUE",
END:VALARM "BEGIN:VALARM",
END:VEVENT "ACTION:DISPLAY",
END:VCALENDAR`; "DESCRIPTION:Reminder",
"TRIGGER:-PT15M",
"END:VALARM",
"END:VEVENT",
"END:VCALENDAR"
];
const icsContent = foldICS(lines.join("\r\n")) + "\r\n";
return { return {
content: icsContent, content: icsContent,
@@ -945,10 +1012,22 @@ END:VCALENDAR`;
}; };
} }
function downloadICS(content, filename) { let icsObjectUrl = null;
/*
* Download URL for the ICS content. Revokes the URL of
* the previous download.
*/
function downloadICS(content) {
const blob = new Blob([content], { type: 'text/calendar;charset=utf-8' }); const blob = new Blob([content], { type: 'text/calendar;charset=utf-8' });
const url = URL.createObjectURL(blob);
return url; if (icsObjectUrl !== null) {
URL.revokeObjectURL(icsObjectUrl);
}
icsObjectUrl = URL.createObjectURL(blob);
return icsObjectUrl;
} }
const MINUTES_STEP = 15; const MINUTES_STEP = 15;
@@ -1159,9 +1238,7 @@ function createMeeting() {
const icsData = generateICS(meetingName, description || meetingName, startTime, endTime, organizer); const icsData = generateICS(meetingName, description || meetingName, startTime, endTime, organizer);
/* Create download link */ /* Create download link */
const blob = new Blob([icsData.content], { type: 'text/calendar;charset=utf-8' }); downloadIcsLink.href = downloadICS(icsData.content);
const url = URL.createObjectURL(blob);
downloadIcsLink.href = url;
downloadIcsLink.download = `${icsData.roomName}.ics`; downloadIcsLink.download = `${icsData.roomName}.ics`;
/* Show success message */ /* Show success message */