- 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) {
|
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 */
|
||||||
|
|||||||
Reference in new issue
Block a user