-
Notifications
You must be signed in to change notification settings - Fork 37
/
index.ts
107 lines (85 loc) · 2.63 KB
/
index.ts
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
import {
BLANK_URL,
ctrlCharactersRegex,
htmlCtrlEntityRegex,
htmlEntitiesRegex,
invalidProtocolRegex,
relativeFirstCharacters,
whitespaceEscapeCharsRegex,
urlSchemeRegex,
} from "./constants";
function isRelativeUrlWithoutProtocol(url: string): boolean {
return relativeFirstCharacters.indexOf(url[0]) > -1;
}
function decodeHtmlCharacters(str: string) {
const removedNullByte = str.replace(ctrlCharactersRegex, "");
return removedNullByte.replace(htmlEntitiesRegex, (match, dec) => {
return String.fromCharCode(dec);
});
}
function isValidUrl(url: string): boolean {
return URL.canParse(url);
}
function decodeURI(uri: string): string {
try {
return decodeURIComponent(uri);
} catch (e: unknown) {
// Ignoring error
// It is possible that the URI contains a `%` not associated
// with URI/URL-encoding.
return uri;
}
}
export function sanitizeUrl(url?: string): string {
if (!url) {
return BLANK_URL;
}
let charsToDecode;
let decodedUrl = decodeURI(url.trim());
do {
decodedUrl = decodeHtmlCharacters(decodedUrl)
.replace(htmlCtrlEntityRegex, "")
.replace(ctrlCharactersRegex, "")
.replace(whitespaceEscapeCharsRegex, "")
.trim();
decodedUrl = decodeURI(decodedUrl);
charsToDecode =
decodedUrl.match(ctrlCharactersRegex) ||
decodedUrl.match(htmlEntitiesRegex) ||
decodedUrl.match(htmlCtrlEntityRegex) ||
decodedUrl.match(whitespaceEscapeCharsRegex);
} while (charsToDecode && charsToDecode.length > 0);
const sanitizedUrl = decodedUrl;
if (!sanitizedUrl) {
return BLANK_URL;
}
if (isRelativeUrlWithoutProtocol(sanitizedUrl)) {
return sanitizedUrl;
}
// Remove any leading whitespace before checking the URL scheme
const trimmedUrl = sanitizedUrl.trimStart();
const urlSchemeParseResults = trimmedUrl.match(urlSchemeRegex);
if (!urlSchemeParseResults) {
return sanitizedUrl;
}
const urlScheme = urlSchemeParseResults[0].toLowerCase().trim();
if (invalidProtocolRegex.test(urlScheme)) {
return BLANK_URL;
}
const backSanitized = trimmedUrl.replace(/\\/g, "/");
// Handle special cases for mailto: and custom deep-link protocols
if (urlScheme === "mailto:" || urlScheme.includes("://")) {
return backSanitized;
}
// For http and https URLs, perform additional validation
if (urlScheme === "http:" || urlScheme === "https:") {
if (!isValidUrl(backSanitized)) {
return BLANK_URL;
}
const url = new URL(backSanitized);
url.protocol = url.protocol.toLowerCase();
url.hostname = url.hostname.toLowerCase();
return url.toString();
}
return backSanitized;
}