- fix escaping in ICS generation
- add CRLF line endings in ICS - add line folding in ICS - fix downloadICS()
This commit is contained in:
1 parent
86caf9a3a4
commit
ce80e52923
1 file changed
+109
-32
+109
-32
@@ -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 */
|
||||
|
||||
Reference in new issue
Block a user