diff --git a/src/calendar-app/calendar/gui/eventpopup/CalendarEventPopup.ts b/src/calendar-app/calendar/gui/eventpopup/CalendarEventPopup.ts index 694fd5110402..000ef5948679 100644 --- a/src/calendar-app/calendar/gui/eventpopup/CalendarEventPopup.ts +++ b/src/calendar-app/calendar/gui/eventpopup/CalendarEventPopup.ts @@ -9,12 +9,12 @@ import { Dialog } from "../../../../common/gui/base/Dialog.js" import { createAsyncDropdown, DROPDOWN_MARGIN, PosRect, showDropdown } from "../../../../common/gui/base/Dropdown.js" import { Keys } from "../../../../common/api/common/TutanotaConstants.js" import type { HtmlSanitizer } from "../../../../common/misc/HtmlSanitizer.js" -import { prepareCalendarDescription } from "../../../../common/calendar/date/CalendarUtils.js" import { BootIcons } from "../../../../common/gui/base/icons/BootIcons.js" import { IconButton } from "../../../../common/gui/base/IconButton.js" import { convertTextToHtml } from "../../../../common/misc/Formatter.js" import { CalendarEventPreviewViewModel } from "./CalendarEventPreviewViewModel.js" import { showDeletePopup } from "../CalendarGuiUtils.js" +import { prepareCalendarDescription } from "../../../../common/api/common/utils/CommonCalendarUtils.js" /** * small modal displaying all relevant information about an event in a compact fashion. offers limited editing capabilities to participants in the diff --git a/src/calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.ts b/src/calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.ts index c69dbb4739ec..e1fec253cc3d 100644 --- a/src/calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.ts +++ b/src/calendar-app/calendar/gui/eventpopup/CalendarEventPreviewViewModel.ts @@ -9,6 +9,8 @@ import m from "mithril" import { clone, Thunk } from "@tutao/tutanota-utils" import { CalendarEventUidIndexEntry } from "../../../../common/api/worker/facades/lazy/CalendarFacade.js" import { EventEditorDialog } from "../eventeditor-view/CalendarEventEditDialog.js" +import { convertTextToHtml } from "../../../../common/misc/Formatter.js" +import { prepareCalendarDescription } from "../../../../common/api/common/utils/CommonCalendarUtils.js" /** * makes decisions about which operations are available from the popup and knows how to implement them depending on the event's type. @@ -215,9 +217,13 @@ export class CalendarEventPreviewViewModel { async sanitizeDescription(): Promise { const { htmlSanitizer } = await import("../../../../common/misc/HtmlSanitizer.js") - this.sanitizedDescription = htmlSanitizer.sanitizeHTML(this.calendarEvent.description, { - blockExternalContent: true, - }).html + this.sanitizedDescription = prepareCalendarDescription( + this.calendarEvent.description, + (s) => + htmlSanitizer.sanitizeHTML(convertTextToHtml(s), { + blockExternalContent: false, + }).html, + ) } getSanitizedDescription() { diff --git a/src/common/api/common/utils/CommonCalendarUtils.ts b/src/common/api/common/utils/CommonCalendarUtils.ts index ad7b8de60f95..70151a1d72f6 100644 --- a/src/common/api/common/utils/CommonCalendarUtils.ts +++ b/src/common/api/common/utils/CommonCalendarUtils.ts @@ -202,3 +202,32 @@ export function isBefore(dateA: Date, dateB: Date, comparisonType: "dateTime" | throw new Error("Unknown comparison method") } } + +/** + * Prepare calendar event description to be shown to the user. + * + * Outlook invitations frequently include links enclosed within "<>" in the email/event description. + * Sanitizing this string can cause the links to be lost. + * To prevent this, we use this function to remove the "<>" characters before applying sanitization whenever necessary. + * + * They look like this: + * ``` + * text + * ``` + * + * @param description Description to clean up + * @param sanitizer Sanitizer to apply after preparing the description + */ +export function prepareCalendarDescription(description: string, sanitizer: (s: string) => string): string { + const prepared = description.replace(/<(http|https):\/\/[A-z0-9$-_.+!*‘(),/?]+>/gi, (possiblyLink) => { + try { + const withoutBrackets = possiblyLink.slice(1, -1) + const url = new URL(withoutBrackets) + return ` ${withoutBrackets}` + } catch (e) { + return possiblyLink + } + }) + + return sanitizer(prepared) +} diff --git a/src/common/calendar/date/CalendarUtils.ts b/src/common/calendar/date/CalendarUtils.ts index 204d85bdcdeb..1026a15c3f21 100644 --- a/src/common/calendar/date/CalendarUtils.ts +++ b/src/common/calendar/date/CalendarUtils.ts @@ -823,32 +823,6 @@ export function findFirstPrivateCalendar(calendarInfo: ReadonlyMap - * ``` - * - * @param description description to clean up - * @param sanitizer optional sanitizer to apply after preparing the description - */ -export function prepareCalendarDescription(description: string, sanitizer: (s: string) => string): string { - const prepared = description.replace(/<(http|https):\/\/[A-z0-9$-_.+!*‘(),/?]+>/gi, (possiblyLink) => { - try { - const withoutBrackets = possiblyLink.slice(1, -1) - const url = new URL(withoutBrackets) - return `${withoutBrackets}` - } catch (e) { - return possiblyLink - } - }) - - return sanitizer(prepared) -} - export const DEFAULT_HOUR_OF_DAY = 6 /** Get CSS class for the date element. */ diff --git a/src/common/misc/SanitizedTextViewModel.ts b/src/common/misc/SanitizedTextViewModel.ts index 3fd0519b932b..2e24f01ebdf5 100644 --- a/src/common/misc/SanitizedTextViewModel.ts +++ b/src/common/misc/SanitizedTextViewModel.ts @@ -1,5 +1,7 @@ import type { HtmlSanitizer } from "./HtmlSanitizer.js" import { noOp } from "@tutao/tutanota-utils" +import { convertTextToHtml } from "./Formatter.js" +import { prepareCalendarDescription } from "../api/common/utils/CommonCalendarUtils.js" export class SanitizedTextViewModel { private sanitizedText: string | null = null @@ -14,7 +16,13 @@ export class SanitizedTextViewModel { get content(): string { if (this.sanitizedText == null) { - this.sanitizedText = this.sanitizer.sanitizeHTML(this.text, { blockExternalContent: false }).html + this.sanitizedText = prepareCalendarDescription( + this.text, + (s) => + this.sanitizer.sanitizeHTML(convertTextToHtml(s), { + blockExternalContent: false, + }).html, + ) } return this.sanitizedText } diff --git a/test/tests/calendar/CalendarUtilsTest.ts b/test/tests/calendar/CalendarUtilsTest.ts index 44e46712c83b..65036800cb25 100644 --- a/test/tests/calendar/CalendarUtilsTest.ts +++ b/test/tests/calendar/CalendarUtilsTest.ts @@ -24,7 +24,6 @@ import { getWeekNumber, isEventBetweenDays, parseAlarmInterval, - prepareCalendarDescription, StandardAlarmInterval, } from "../../../src/common/calendar/date/CalendarUtils.js" import { lang } from "../../../src/common/misc/LanguageViewModel.js" @@ -32,7 +31,12 @@ import { DateWrapperTypeRef, GroupMembershipTypeRef, GroupTypeRef, UserTypeRef } import { AccountType, EndType, GroupType, RepeatPeriod, ShareCapability } from "../../../src/common/api/common/TutanotaConstants.js" import { timeStringFromParts } from "../../../src/common/misc/Formatter.js" import { DateTime } from "luxon" -import { generateEventElementId, getAllDayDateUTC, serializeAlarmInterval } from "../../../src/common/api/common/utils/CommonCalendarUtils.js" +import { + generateEventElementId, + getAllDayDateUTC, + prepareCalendarDescription, + serializeAlarmInterval, +} from "../../../src/common/api/common/utils/CommonCalendarUtils.js" import { hasCapabilityOnGroup } from "../../../src/common/sharing/GroupUtils.js" import { CalendarEvent, @@ -491,7 +495,7 @@ o.spec("calendar utils tests", function () { o.spec("prepareCalendarDescription", function () { o("angled link replaced with a proper link", function () { o(prepareCalendarDescription("JoinBlahBlah", identity)).equals( - `JoinBlahBlahhttps://the-link.com/path`, + `JoinBlahBlah https://the-link.com/path`, ) }) o("normal HTML link is not touched", function () {