-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
114 lines (94 loc) · 4.64 KB
/
index.js
File metadata and controls
114 lines (94 loc) · 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* External link confirmation (single delegated listener)
* - Intercepts outbound (external) http/https navigations
* - Shows a <dialog> modal asking the user to confirm leaving the site
* - Opens the external destination safely in a new tab
*
* Security hardening included:
* - Only allows http/https schemes (blocks javascript:, data:, etc.)
* - Blocks credentialed URLs (https://user:pass@host) to reduce phishing/abuse
* - Uses noopener,noreferrer when opening a new tab (prevents reverse-tabnabbing + hides referrer)
* - Stores pending URL in a closure variable (not dataset) to avoid easy DOM tampering
*/
// Domains you want to treat as "allowed" (no confirmation modal even though they may be external).
// Use bare hostnames (no protocol). Add both root + subdomains if needed.
const IGNORED_DOMAINS = new Set([
"external-link-exit-confirmation.webflow.io",
]);
// Cache DOM references once (avoid repeated querySelector calls on every click)
const modal = document.querySelector("dialog"); // your confirmation modal (<dialog>)
const continueButton = modal?.querySelector("#dialog-continue"); // the button inside the dialog that proceeds
// Holds the URL the user is about to visit.
// We keep this in JS memory (closure) rather than element.dataset for tamper resistance.
let pendingUrl = null;
/**
* Wire the "Continue" button once.
* When clicked, it opens the stored pendingUrl in a new tab safely.
*/
if (continueButton) {
continueButton.addEventListener("click", () => {
if (!pendingUrl) return; // nothing to do if no URL is pending
// Open safely:
// - noopener: prevents the new page from controlling window.opener (reverse-tabnabbing)
// - noreferrer: prevents sending the Referer header (often desirable for external exits)
window.open(pendingUrl, "_blank", "noopener,noreferrer");
// Clear state and close modal
pendingUrl = null;
modal?.close();
});
}
/**
* Single delegated click listener:
* This avoids attaching listeners to every link/button (most performant approach for many elements).
*/
document.addEventListener("click", (e) => {
// ---- 1) Only intercept normal left-clicks (preserves expected browser behavior) ----
if (e.defaultPrevented) return; // another script already handled this click
if (e.button !== 0) return; // not a left click (ignore middle/right clicks)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // user intends special behavior (new tab, etc.)
// ---- 2) Find the nearest clickable link/button that implies navigation ----
// Supports:
// - <a href="...">
// - <button formaction="...">
const target = e.target.closest("a[href], button[formaction]");
if (!target) return;
// ---- 3) Extract the raw URL from the element attribute ----
const rawUrl =
target.tagName === "A"
? target.getAttribute("href")
: target.getAttribute("formaction");
if (!rawUrl) return; // ignore missing/empty URLs
// Fast ignore for same-page hash navigation (e.g. "#section")
if (rawUrl.startsWith("#")) return;
// ---- 4) Parse and normalize the URL safely ----
// Using base `location.href` means relative URLs are resolved correctly.
let urlObj;
try {
urlObj = new URL(rawUrl, location.href);
} catch {
// If it's not a valid URL, do nothing
return;
}
// ---- 5) Security: allow only http/https navigation ----
// Blocks schemes like javascript:, data:, file:, etc.
if (urlObj.protocol !== "http:" && urlObj.protocol !== "https:") return;
// ---- 6) Security: block URLs containing credentials ----
// Example: https://user:pass@example.com (often used for phishing tricks)
if (urlObj.username || urlObj.password) return;
// ---- 7) Skip internal links (same origin) ----
// This is the most correct check for "my domain" in modern web apps.
if (urlObj.origin === location.origin) return;
// ---- 8) Skip allowlisted domains ----
// Normalize hostname by removing leading "www."
const hostname = urlObj.hostname.replace(/^www\./, "");
if (IGNORED_DOMAINS.has(hostname)) return;
// ---- 9) Show the confirmation dialog and store the destination ----
// Only if the dialog API is available and the continue button exists.
if (modal && typeof modal.showModal === "function" && continueButton) {
e.preventDefault(); // stop immediate navigation
// Store the URL in memory for the Continue button handler to use
pendingUrl = urlObj.href;
// Open the modal (native <dialog> behavior)
modal.showModal();
}
});