Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1122c116b6
commit
e3c0e11eb1
8 files changed
+7
-370
No files matched your search
@@ -1646,11 +1646,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
(hasActiveTeamPlan || isSMSAction(step.action))
|
||||
}
|
||||
excludedToolbarItems={
|
||||
isSMSAction(step.action)
|
||||
? ["blockType", "bold", "italic", "link"]
|
||||
: isOrganization
|
||||
? []
|
||||
: ["link"]
|
||||
!isSMSAction(step.action) ? [] : ["blockType", "bold", "italic", "link"]
|
||||
}
|
||||
plainText={isSMSAction(step.action)}
|
||||
/>
|
||||
|
||||
@@ -35,7 +35,6 @@ import type { VariablesType } from "../lib/reminders/templates/customTemplate";
|
||||
import customTemplate from "../lib/reminders/templates/customTemplate";
|
||||
import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate";
|
||||
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
|
||||
import { replaceCloakedLinksInHtml } from "../lib/reminders/utils";
|
||||
|
||||
export async function handler(req: NextRequest) {
|
||||
const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey");
|
||||
@@ -355,17 +354,10 @@ export async function handler(req: NextRequest) {
|
||||
title: booking.title || booking.eventType?.title || "",
|
||||
};
|
||||
|
||||
// Organization accounts are allowed to use cloaked links (URL behind text)
|
||||
// since they are paid accounts with lower spam/scam risk
|
||||
const isOrganization = reminder.workflowStep?.workflow?.team?.isOrganization ?? false;
|
||||
const processedEmailBody = isOrganization
|
||||
? emailContent.emailBody
|
||||
: replaceCloakedLinksInHtml(emailContent.emailBody);
|
||||
|
||||
const mailData = {
|
||||
subject: emailContent.emailSubject,
|
||||
to: Array.isArray(sendTo) ? sendTo : [sendTo],
|
||||
html: processedEmailBody,
|
||||
html: emailContent.emailBody,
|
||||
attachments: reminder.workflowStep.includeCalendarEvent
|
||||
? [
|
||||
{
|
||||
@@ -461,17 +453,10 @@ export async function handler(req: NextRequest) {
|
||||
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
||||
const batchId = isSendgridEnabled ? await getBatchId() : undefined;
|
||||
|
||||
// Organization accounts are allowed to use cloaked links (URL behind text)
|
||||
// since they are paid accounts with lower spam/scam risk
|
||||
const isOrganization = reminder.workflowStep?.workflow?.team?.isOrganization ?? false;
|
||||
const processedEmailBody = isOrganization
|
||||
? emailContent.emailBody
|
||||
: replaceCloakedLinksInHtml(emailContent.emailBody);
|
||||
|
||||
const mailData = {
|
||||
subject: emailContent.emailSubject,
|
||||
to: [sendTo],
|
||||
html: processedEmailBody,
|
||||
html: emailContent.emailBody,
|
||||
sender: reminder.workflowStep?.sender,
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
|
||||
@@ -8,9 +8,6 @@ type PartialWorkflowStep =
|
||||
workflow: {
|
||||
userId?: number;
|
||||
teamId?: number;
|
||||
team?: {
|
||||
isOrganization: boolean;
|
||||
} | null;
|
||||
};
|
||||
})
|
||||
| null;
|
||||
@@ -150,11 +147,6 @@ export const select = {
|
||||
select: {
|
||||
userId: true,
|
||||
teamId: true,
|
||||
team: {
|
||||
select: {
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,14 +8,13 @@ import logger from "@calcom/lib/logger";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { TimeUnit } from "@calcom/prisma/enums";
|
||||
import { WorkflowMethods, WorkflowTriggerEvents, type WorkflowTemplates } from "@calcom/prisma/enums";
|
||||
import { WorkflowMethods, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
import type { BookingInfo, FormSubmissionData, ScheduleEmailReminderAction } from "../types";
|
||||
import type { BookingInfo, ScheduleEmailReminderAction, FormSubmissionData } from "../types";
|
||||
import { sendOrScheduleWorkflowEmails } from "./providers/emailProvider";
|
||||
import type { WorkflowContextData } from "./reminderScheduler";
|
||||
import type { VariablesType } from "./templates/customTemplate";
|
||||
import customTemplate, { transformRoutingFormResponsesToVariableFormat } from "./templates/customTemplate";
|
||||
import { replaceCloakedLinksInHtml } from "./utils";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[emailReminderManager]"] });
|
||||
|
||||
@@ -39,7 +38,6 @@ type scheduleEmailReminderArgs = ScheduleReminderArgs & {
|
||||
hideBranding?: boolean;
|
||||
includeCalendarEvent?: boolean;
|
||||
verifiedAt: Date | null;
|
||||
isOrganization?: boolean;
|
||||
};
|
||||
|
||||
type SendEmailReminderParams = {
|
||||
@@ -117,7 +115,6 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
|
||||
hideBranding,
|
||||
includeCalendarEvent,
|
||||
action,
|
||||
isOrganization,
|
||||
} = args;
|
||||
|
||||
const uid = evt.uid as string;
|
||||
@@ -155,7 +152,6 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e
|
||||
template,
|
||||
includeCalendarEvent,
|
||||
triggerEvent,
|
||||
isOrganization,
|
||||
});
|
||||
|
||||
await sendOrScheduleWorkflowEmailWithReminder({
|
||||
@@ -184,7 +180,6 @@ const scheduleEmailReminderForForm = async (
|
||||
emailSubject = "",
|
||||
emailBody = "",
|
||||
hideBranding,
|
||||
isOrganization,
|
||||
} = args;
|
||||
|
||||
const emailContent = {
|
||||
@@ -213,15 +208,9 @@ const scheduleEmailReminderForForm = async (
|
||||
// Allows debugging generated email content without waiting for sendgrid to send emails
|
||||
log.debug(`Sending Email for trigger ${triggerEvent}`, JSON.stringify(emailContent));
|
||||
|
||||
// Organization accounts are allowed to use cloaked links (URL behind text)
|
||||
// since they are paid accounts with lower spam/scam risk
|
||||
const processedEmailBody = isOrganization
|
||||
? emailContent.emailBody
|
||||
: replaceCloakedLinksInHtml(emailContent.emailBody);
|
||||
|
||||
const mailData = {
|
||||
subject: emailContent.emailSubject,
|
||||
html: processedEmailBody,
|
||||
html: emailContent.emailBody,
|
||||
sender,
|
||||
};
|
||||
|
||||
|
||||
@@ -115,74 +115,6 @@ export const getAttendeeToBeUsedInSMS = (
|
||||
return attendeeToBeUsedInSMS;
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes HTML special characters to prevent XSS when inserting text into HTML.
|
||||
*/
|
||||
const escapeHtml = (str: string): string =>
|
||||
str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
|
||||
/**
|
||||
* Escapes HTML attribute values to prevent attribute injection.
|
||||
*/
|
||||
const escapeHtmlAttribute = (str: string): string =>
|
||||
str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
/**
|
||||
* Replaces cloaked links in HTML content with visible URLs.
|
||||
* This ensures recipients can see the actual destination of links,
|
||||
* helping them identify potentially malicious URLs.
|
||||
*
|
||||
* Transforms: <a href="https://example.com">Click here</a>
|
||||
* Into: <a href="https://example.com">https://example.com</a>
|
||||
*
|
||||
* Also handles nested HTML tags like: <a href="https://example.com"><b>Click here</b></a>
|
||||
* Handles incomplete hrefs: <a href="https://">https://example.com</a>
|
||||
*/
|
||||
export const replaceCloakedLinksInHtml = (html: string): string => {
|
||||
// Match anchor tags with href attribute
|
||||
// Captures: attributes with href, href value, inner content (including nested HTML)
|
||||
const anchorRegex = /<a\s+([^>]*href=["']([^"']+)["'][^>]*)>([\s\S]*?)<\/a>/gi;
|
||||
|
||||
return html.replace(anchorRegex, (match, attributes, href, innerContent) => {
|
||||
// Strip HTML tags from inner content to get plain text for comparison
|
||||
const linkText = innerContent.replace(/<[^>]*>/g, "").trim();
|
||||
|
||||
// Check if href is incomplete (just protocol like "https://" or "http://")
|
||||
const isIncompleteHref = /^https?:\/\/?$/.test(href);
|
||||
|
||||
// If href is incomplete and link text contains a valid URL, use the link text URL
|
||||
let actualUrl = href;
|
||||
let hrefWasUpdated = false;
|
||||
if (isIncompleteHref && linkText) {
|
||||
const urlPattern = /^https?:\/\/[^\s]+/i;
|
||||
const urlMatch = linkText.match(urlPattern);
|
||||
if (urlMatch) {
|
||||
actualUrl = urlMatch[0];
|
||||
attributes = attributes.replace(/href=["'][^"']+["']/, `href="${escapeHtmlAttribute(actualUrl)}"`);
|
||||
hrefWasUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the link text is already the URL (or very similar), keep it as is
|
||||
const normalizedHref = actualUrl.toLowerCase().replace(/\/$/, "");
|
||||
const normalizedText = linkText.toLowerCase().trim().replace(/\/$/, "");
|
||||
|
||||
if (!hrefWasUpdated && (normalizedText === normalizedHref || normalizedText === normalizedHref.replace(/^https?:\/\//, ""))) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Replace the link text with the actual URL, escaping HTML to prevent XSS
|
||||
return `<a ${attributes}>${escapeHtml(actualUrl)}</a>`;
|
||||
});
|
||||
};
|
||||
|
||||
export const shouldUseTwilio = (trigger: WorkflowTriggerEvents, scheduledDate: dayjs.Dayjs | null) => {
|
||||
if (IMMEDIATE_WORKFLOW_TRIGGER_EVENTS.includes(trigger)) {
|
||||
return true;
|
||||
|
||||
@@ -28,7 +28,6 @@ import type { VariablesType } from "../reminders/templates/customTemplate";
|
||||
import customTemplate, {
|
||||
transformBookingResponsesToVariableFormat,
|
||||
} from "../reminders/templates/customTemplate";
|
||||
import { replaceCloakedLinksInHtml } from "../reminders/utils";
|
||||
import emailRatingTemplate from "../reminders/templates/emailRatingTemplate";
|
||||
import emailReminderTemplate from "../reminders/templates/emailReminderTemplate";
|
||||
import type {
|
||||
@@ -69,7 +68,6 @@ export class EmailWorkflowService {
|
||||
}
|
||||
|
||||
const workflow = workflowReminder.workflowStep.workflow;
|
||||
const isOrganization = workflow.team?.isOrganization ?? false;
|
||||
|
||||
let emailAttendeeSendToOverride: string | null = null;
|
||||
if (workflowReminder.seatReferenceId) {
|
||||
@@ -111,7 +109,6 @@ export class EmailWorkflowService {
|
||||
action: workflowReminder.workflowStep.action as ScheduleEmailReminderAction,
|
||||
template: workflowReminder.workflowStep.template,
|
||||
includeCalendarEvent: workflowReminder.workflowStep.includeCalendarEvent,
|
||||
isOrganization,
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
@@ -252,7 +249,6 @@ export class EmailWorkflowService {
|
||||
template,
|
||||
includeCalendarEvent,
|
||||
triggerEvent,
|
||||
isOrganization,
|
||||
}: {
|
||||
evt: BookingInfo;
|
||||
sendTo: string[];
|
||||
@@ -265,7 +261,6 @@ export class EmailWorkflowService {
|
||||
template?: WorkflowTemplates;
|
||||
includeCalendarEvent?: boolean;
|
||||
triggerEvent: WorkflowTriggerEvents;
|
||||
isOrganization?: boolean;
|
||||
}) {
|
||||
const log = logger.getSubLogger({
|
||||
prefix: [`[generateEmailPayloadForEvtWorkflow]: bookingUid: ${evt?.uid}`],
|
||||
@@ -549,15 +544,9 @@ export class EmailWorkflowService {
|
||||
const customReplyToEmail =
|
||||
evt?.eventType?.customReplyToEmail || (evt as CalendarEvent).customReplyToEmail;
|
||||
|
||||
// Organization accounts are allowed to use cloaked links (URL behind text)
|
||||
// since they are paid accounts with lower spam/scam risk
|
||||
const processedEmailBody = isOrganization
|
||||
? emailContent.emailBody
|
||||
: replaceCloakedLinksInHtml(emailContent.emailBody);
|
||||
|
||||
return {
|
||||
subject: emailContent.emailSubject,
|
||||
html: processedEmailBody,
|
||||
html: emailContent.emailBody,
|
||||
...(!evt.hideOrganizerEmail && {
|
||||
replyTo: customReplyToEmail || evt.organizer.email,
|
||||
}),
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { replaceCloakedLinksInHtml } from "../reminders/utils";
|
||||
|
||||
describe("replaceCloakedLinksInHtml", () => {
|
||||
describe("basic cloaked links", () => {
|
||||
test("should replace cloaked link text with the actual URL", () => {
|
||||
const html = '<a href="https://example.com">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should replace cloaked link with different text", () => {
|
||||
const html = '<a href="https://malicious-site.com">Visit our safe website</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://malicious-site.com">https://malicious-site.com</a>');
|
||||
});
|
||||
|
||||
test("should handle links with additional attributes", () => {
|
||||
const html = '<a href="https://example.com" target="_blank" rel="noopener">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com" target="_blank" rel="noopener">https://example.com</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("nested HTML tags", () => {
|
||||
test("should handle bold text inside link", () => {
|
||||
const html = '<a href="https://example.com"><b>Click here</b></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle strong text inside link", () => {
|
||||
const html = '<a href="https://example.com"><strong>Click here</strong></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle span inside link", () => {
|
||||
const html = '<a href="https://example.com"><span class="link-text">Click here</span></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle multiple nested tags", () => {
|
||||
const html = '<a href="https://example.com"><span><strong>Click here</strong></span></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle nested tags with visible URL - should remain unchanged", () => {
|
||||
const html = '<a href="https://example.com"><strong>https://example.com</strong></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com"><strong>https://example.com</strong></a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("already-visible URLs", () => {
|
||||
test("should not modify link when text matches URL exactly", () => {
|
||||
const html = '<a href="https://example.com">https://example.com</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should not modify link when text matches URL without protocol", () => {
|
||||
const html = '<a href="https://example.com">example.com</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">example.com</a>');
|
||||
});
|
||||
|
||||
test("should not modify link when text matches URL with trailing slash difference", () => {
|
||||
const html = '<a href="https://example.com/">https://example.com</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com/">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle case-insensitive URL comparison", () => {
|
||||
const html = '<a href="https://Example.COM">https://example.com</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://Example.COM">https://example.com</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty link text", () => {
|
||||
test("should replace empty link text with URL", () => {
|
||||
const html = '<a href="https://example.com"></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should replace whitespace-only link text with URL", () => {
|
||||
const html = '<a href="https://example.com"> </a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple links in content", () => {
|
||||
test("should replace all cloaked links in content", () => {
|
||||
const html =
|
||||
'<p>Visit <a href="https://site1.com">here</a> and <a href="https://site2.com">there</a></p>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<p>Visit <a href="https://site1.com">https://site1.com</a> and <a href="https://site2.com">https://site2.com</a></p>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle mix of cloaked and visible links", () => {
|
||||
const html =
|
||||
'<p><a href="https://visible.com">https://visible.com</a> and <a href="https://hidden.com">Click me</a></p>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<p><a href="https://visible.com">https://visible.com</a> and <a href="https://hidden.com">https://hidden.com</a></p>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle multiple nested HTML links", () => {
|
||||
const html =
|
||||
'<div><a href="https://a.com"><b>Link A</b></a><a href="https://b.com"><i>Link B</i></a></div>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<div><a href="https://a.com">https://a.com</a><a href="https://b.com">https://b.com</a></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("incomplete hrefs", () => {
|
||||
test("should use URL from link text when href is incomplete (https://)", () => {
|
||||
const html =
|
||||
'<a href="https://" rel="noopener" class="editor-link"><span>https://bristolwills.com/prepare-for-will-appointment</span><br></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<a href="https://bristolwills.com/prepare-for-will-appointment" rel="noopener" class="editor-link">https://bristolwills.com/prepare-for-will-appointment</a>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should use URL from link text when href is incomplete (http://)", () => {
|
||||
const html = '<a href="http://">http://example.com/page</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="http://example.com/page">http://example.com/page</a>');
|
||||
});
|
||||
|
||||
test("should handle incomplete href with nested HTML tags", () => {
|
||||
const html = '<a href="https://"><span>https://example.com</span></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle incomplete href with additional attributes", () => {
|
||||
const html =
|
||||
'<a href="https://" target="_blank" rel="noopener">https://example.com/path</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<a href="https://example.com/path" target="_blank" rel="noopener">https://example.com/path</a>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should not modify if link text doesn't contain a valid URL", () => {
|
||||
const html = '<a href="https://">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://">https://</a>');
|
||||
});
|
||||
|
||||
test("should handle incomplete href when link text already matches", () => {
|
||||
const html = '<a href="https://">https://example.com</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should escape quotes in URL when fixing incomplete href to prevent attribute injection", () => {
|
||||
const html = '<a href="https://">https://example.com?q="test"</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com?q="test"">https://example.com?q="test"</a>');
|
||||
});
|
||||
|
||||
test("should escape single quotes in URL when fixing incomplete href attribute", () => {
|
||||
const html = "<a href='https://'>https://example.com?q='test'</a>";
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
// Single quotes in href attribute are escaped, but in link text they don't need escaping
|
||||
expect(result).toBe("<a href=\"https://example.com?q='test'\">https://example.com?q='test'</a>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("should handle links with single quotes in href", () => {
|
||||
const html = "<a href='https://example.com'>Click here</a>";
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe("<a href='https://example.com'>https://example.com</a>");
|
||||
});
|
||||
|
||||
test("should handle multiline link content", () => {
|
||||
const html = `<a href="https://example.com">
|
||||
Click
|
||||
here
|
||||
</a>`;
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should preserve surrounding content", () => {
|
||||
const html = '<p>Before</p><a href="https://example.com">Link</a><p>After</p>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<p>Before</p><a href="https://example.com">https://example.com</a><p>After</p>');
|
||||
});
|
||||
|
||||
test("should handle content with no links", () => {
|
||||
const html = "<p>No links here</p>";
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe("<p>No links here</p>");
|
||||
});
|
||||
|
||||
test("should handle empty string", () => {
|
||||
const html = "";
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
test("should escape HTML special characters in href to prevent XSS", () => {
|
||||
// Test case for potential XSS vector where href contains HTML special characters
|
||||
const html = '<a href="https://example.com?foo=1&bar=2">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com?foo=1&bar=2">https://example.com?foo=1&bar=2</a>');
|
||||
});
|
||||
|
||||
test("should escape angle brackets in href", () => {
|
||||
const html = '<a href="https://example.com/<script>alert(1)</script>">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<a href="https://example.com/<script>alert(1)</script>">https://example.com/<script>alert(1)</script></a>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should escape quotes in href", () => {
|
||||
const html = '<a href="https://example.com?q="test"">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<a href="https://example.com?q="test"">https://example.com?q=&quot;test&quot;</a>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -134,11 +134,6 @@ export class WorkflowReminderRepository {
|
||||
timeUnit: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
team: {
|
||||
select: {
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user