fix(projects): make email preview fully read-only (no clickable links)
Inside the preview iframe the Accept button navigated but other links didn't — inconsistent, and worse, clicking Accept/Decline would hit the live action URLs and change the quote state. Sandbox the iframe (no popups/scripts/forms) and force all anchors to target=_blank so every link is inert. Now nothing in the preview is clickable (consistent + safe); scrolling and brand colors are unaffected.
This commit is contained in:
@@ -83,6 +83,16 @@ const KIND_ICON: Record<FeedKind, React.ComponentType<{ className?: string }>> =
|
|||||||
hours: Clock,
|
hours: Clock,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Neutralise links for a read-only preview: force every anchor to target a
|
||||||
|
* new tab so the sandboxed iframe (no allow-popups) blocks the navigation
|
||||||
|
* entirely. Without this, clicking "Accept"/"Decline" in the preview would
|
||||||
|
* hit the live action URLs and actually change the quote's state. */
|
||||||
|
function neutralizeLinks(html: string): string {
|
||||||
|
const base = '<base target="_blank">';
|
||||||
|
if (/<head[^>]*>/i.test(html)) return html.replace(/<head[^>]*>/i, (m) => m + base);
|
||||||
|
return base + html;
|
||||||
|
}
|
||||||
|
|
||||||
function minutesToHours(min: number): string {
|
function minutesToHours(min: number): string {
|
||||||
const h = Math.floor(min / 60);
|
const h = Math.floor(min / 60);
|
||||||
const m = min % 60;
|
const m = min % 60;
|
||||||
@@ -445,13 +455,16 @@ export const ProjectCockpitPage: React.FC = () => {
|
|||||||
{t('projects.email.reRendered', 'Re-rendered from the current template — this email was sent before previews were captured, so it may differ slightly from what the recipient received.')}
|
{t('projects.email.reRendered', 'Re-rendered from the current template — this email was sent before previews were captured, so it may differ slightly from what the recipient received.')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Render the email exactly as sent — it carries its own
|
{/* Read-only preview: renders the email with its own brand
|
||||||
background from the brand/email theme. isolate the
|
colors (color-scheme:normal stops the dark app theme from
|
||||||
iframe's color-scheme so the admin's OS dark mode doesn't
|
tinting it), but `sandbox` (no allow-popups/scripts/forms)
|
||||||
tint a document that defines its own colors. */}
|
+ neutralizeLinks make every link inert — so the admin
|
||||||
|
can't accidentally trigger the live Accept/Decline URLs by
|
||||||
|
clicking inside the preview. Scrolling still works. */}
|
||||||
<iframe
|
<iframe
|
||||||
title="email-preview"
|
title="email-preview"
|
||||||
srcDoc={preview.html}
|
srcDoc={neutralizeLinks(preview.html)}
|
||||||
|
sandbox=""
|
||||||
style={{ colorScheme: 'normal' }}
|
style={{ colorScheme: 'normal' }}
|
||||||
className="w-full h-[60vh] border border-neutral-200 dark:border-neutral-700 rounded"
|
className="w-full h-[60vh] border border-neutral-200 dark:border-neutral-700 rounded"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user