- 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) {
/* Generate a unique room name from the meeting name */
const roomName = createRoomName(meetingName);
@@ -911,32 +971,39 @@ function generateICS(meetingName, description, startTime, endTime, organizer) {
const dtStart = formatICSDate(startTime);
const dtEnd = formatICSDate(endTime);
/* Build ICS content */
const icsContent = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//BNVB//Jitsi Meeting//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
UID:${uid}
DTSTAMP:${dtStamp}
DTSTART:${dtStart}
DTEND:${dtEnd}
SUMMARY:${meetingName.replace(/,/g, '\\,')}
DESCRIPTION:${description.replace(/,/g, '\\,').replace(/\n/g, '\\n')}
LOCATION:${meetingUrl}
ORGANIZER:MAILTO:${organizer}
STATUS:CONFIRMED
URL:${meetingUrl}
SEQUENCE:0
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER:-PT15M
END:VALARM
END:VEVENT
END:VCALENDAR`;
/*
* One property per line, CRLF endings, folded at
* 75 octets (RFC 5545).
*/
const lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
`PRODID:-//Jitsi Meeting Launcher//NONSGML (${CONFIG.jitsiLauncher})//EN`,
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
"BEGIN:VEVENT",
`UID:${uid}`,
`DTSTAMP:${dtStamp}`,
`DTSTART:${dtStart}`,
`DTEND:${dtEnd}`,
`SUMMARY:${escapeICS(meetingName)}`,
`DESCRIPTION:${escapeICS(description)}`,
`LOCATION:${meetingUrl}`,
`ORGANIZER:MAILTO:${organizer}`,
"STATUS:CONFIRMED",
`URL:${meetingUrl}`,
"SEQUENCE:0",
"TRANSP:OPAQUE",
"BEGIN:VALARM",
"ACTION:DISPLAY",
"DESCRIPTION:Reminder",
"TRIGGER:-PT15M",
"END:VALARM",
"END:VEVENT",
"END:VCALENDAR"
];
const icsContent = foldICS(lines.join("\r\n")) + "\r\n";
return {
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 url = URL.createObjectURL(blob);
return url;
if (icsObjectUrl !== null) {
URL.revokeObjectURL(icsObjectUrl);
}
icsObjectUrl = URL.createObjectURL(blob);
return icsObjectUrl;
}
const MINUTES_STEP = 15;
@@ -1159,9 +1238,7 @@ function createMeeting() {
const icsData = generateICS(meetingName, description || meetingName, startTime, endTime, organizer);
/* Create download link */
const blob = new Blob([icsData.content], { type: 'text/calendar;charset=utf-8' });
const url = URL.createObjectURL(blob);
downloadIcsLink.href = url;
downloadIcsLink.href = downloadICS(icsData.content);
downloadIcsLink.download = `${icsData.roomName}.ics`;
/* Show success message */