UNPKG

8.43 kBPlain TextView Raw
1// Copyright 2021 Fastly, Inc.
2
3import {
4 CHARCODE,
5 isCZeroControlPercentEncodeSet,
6 isFragmentPercentEncodeSet,
7 isQueryPercentEncodeSet,
8 isPathPercentEncodeSet,
9 isUserInfoPercentEncodeSet,
10} from "./charcode";
11
12import { SPECIAL_SCHEMES, throwInvalidUrlError } from "./util";
13
14import { URLParser } from "./url-parse";
15
16import { URLProperties } from "./url-properties";
17
18// A URL cannot have a username/password/port
19// if its host is null or the empty string,
20// its cannot-be-a-base-URL flag is set, or its scheme is "file".
21// https://url.spec.whatwg.org/#url-miscellaneous
22function urlCannotHaveUsernamePasswordPort(urlProps: URLProperties): boolean {
23 if (
24 urlProps.hostname.length == 0 ||
25 !SPECIAL_SCHEMES.includes(urlProps.protocol) ||
26 urlProps.protocol == "file:"
27 ) {
28 return true;
29 }
30
31 return false;
32}
33
34// If this’s URL’s cannot-be-a-base-URL flag is set, then return.
35// https://url.spec.whatwg.org/#url-cannot-be-a-base-url-flag
36function urlCannotBeBaseUrl(urlProps: URLProperties): boolean {
37 if (!SPECIAL_SCHEMES.includes(urlProps.protocol)) {
38 return true;
39 }
40
41 return false;
42}
43
44export class URL {
45 private _urlProps: URLProperties;
46
47 constructor(url: string, baseUrl: string | null = null) {
48 // Create our url properties
49 this._urlProps = new URLProperties();
50
51 // Replace Forward Slashes with Back Slashes
52 url = url.replaceAll("\\", "/");
53 let baseUrlString = "";
54 if (baseUrl !== null) {
55 baseUrlString = (baseUrl as string).replaceAll("\\", "/");
56 }
57
58 // First apply our baseUrl if it is absolute
59 if (baseUrlString.length > 0) {
60 URLParser.parseAbsoluteUrl(baseUrl as string, this._urlProps);
61
62 // Check if our relative URL has a protocol
63 let relativeUrlProtocol = "";
64 if (
65 !url.startsWith("//") &&
66 !url.includes("file:") &&
67 url.indexOf(":") > 0
68 ) {
69 relativeUrlProtocol = url.slice(0, url.indexOf(":") + 1);
70 }
71
72 if (
73 relativeUrlProtocol != "" &&
74 !SPECIAL_SCHEMES.includes(relativeUrlProtocol)
75 ) {
76 // treat it as it's own absolute url
77 URLParser.parseAbsoluteUrl(url, this._urlProps);
78 } else {
79 let relativeUrl = url;
80 if (relativeUrlProtocol != "") {
81 // Remove the protocol and pass a relative url
82 relativeUrl = relativeUrl.slice(relativeUrl.indexOf(":") + 1);
83 }
84
85 URLParser.applySchemeOrPathRelativeUrl(relativeUrl, this._urlProps);
86 }
87 } else {
88 // If we didn't have a baseUrl, the url has to be absolute
89 if (!URLParser.isAbsoluteUrl(url)) {
90 throw new Error(
91 "The URL: " +
92 url +
93 " is a relative URL. You must also pass a baseUrl for relative URLs."
94 );
95 return;
96 }
97
98 URLParser.parseAbsoluteUrl(url, this._urlProps);
99 }
100
101 // Run our URL validation
102 // This will throw an error if the URL is invalid
103 URLParser.validateUrl(this._urlProps);
104 }
105
106 // Getters and Setters for properties
107 get protocol(): string {
108 return this._urlProps.protocol;
109 }
110
111 set protocol(protocol: string) {
112 if (!protocol.endsWith(":")) {
113 protocol += ":";
114 }
115
116 // Check if we are changing from a non-special protocol,
117 // to a special protocol
118 // https://nodejs.org/api/url.html#url_special_schemes
119 if (
120 SPECIAL_SCHEMES.includes(this._urlProps.protocol) &&
121 !SPECIAL_SCHEMES.includes(protocol)
122 ) {
123 // Do Nothing
124 } else if (
125 !SPECIAL_SCHEMES.includes(this._urlProps.protocol) &&
126 SPECIAL_SCHEMES.includes(protocol)
127 ) {
128 // Do Nothing
129 } else {
130 this._urlProps.protocol = protocol;
131 }
132 }
133
134 get username(): string {
135 return this._urlProps.username;
136 }
137
138 set username(username: string) {
139 if (urlCannotHaveUsernamePasswordPort(this._urlProps)) {
140 return;
141 }
142
143 this._urlProps.username = username;
144 }
145
146 get password(): string {
147 return this._urlProps.password;
148 }
149
150 set password(password: string) {
151 if (urlCannotHaveUsernamePasswordPort(this._urlProps)) {
152 return;
153 }
154
155 this._urlProps.password = password;
156 }
157
158 get hostname(): string {
159 // The hostname must be all lowercase
160 let lowercaseHostname = "";
161
162 for (let i = 0; i < this._urlProps.hostname.length; i++) {
163 let charcode = this._urlProps.hostname.charCodeAt(i);
164 if (
165 charcode >= CHARCODE.UPPERCASE_A &&
166 charcode <= CHARCODE.UPPERCASE_Z
167 ) {
168 lowercaseHostname += String.fromCharCode(charcode + 32);
169 } else {
170 lowercaseHostname += this._urlProps.hostname.charAt(i);
171 }
172 }
173
174 // We must also percent encode the hostname
175 let encodedHostname = "";
176
177 for (let i = 0; i < lowercaseHostname.length; i++) {
178 let charcode = lowercaseHostname.charCodeAt(i);
179 if (isPathPercentEncodeSet(charcode)) {
180 encodedHostname += "%" + charcode.toString(16);
181 } else {
182 encodedHostname += lowercaseHostname.charAt(i);
183 }
184 }
185
186 return encodedHostname;
187 }
188
189 set hostname(hostname: string) {
190 if (urlCannotBeBaseUrl(this._urlProps)) {
191 return;
192 }
193
194 this._urlProps.hostname = hostname;
195 }
196
197 get port(): string {
198 return this._urlProps.port;
199 }
200
201 set port(port: string) {
202 if (urlCannotHaveUsernamePasswordPort(this._urlProps)) {
203 return;
204 }
205
206 this._urlProps.port = port;
207 }
208
209 get pathname(): string {
210 if (this._urlProps.pathname.length === 0) {
211 return "/";
212 }
213
214 // Percent encode out pathname
215 let encodedPathname = "";
216 for (let i = 0; i < this._urlProps.pathname.length; i++) {
217 let charcode = this._urlProps.pathname.charCodeAt(i);
218 if (isPathPercentEncodeSet(charcode)) {
219 encodedPathname += "%" + charcode.toString(16);
220 } else {
221 encodedPathname += this._urlProps.pathname.charAt(i);
222 }
223 }
224
225 return encodedPathname;
226 }
227
228 set pathname(pathname: string) {
229 if (urlCannotBeBaseUrl(this._urlProps)) {
230 return;
231 }
232
233 this._urlProps.pathname = pathname;
234
235 // Remove any trailing slash
236 if (this._urlProps.pathname.endsWith("/")) {
237 this._urlProps.pathname = this._urlProps.pathname.slice(
238 0,
239 this._urlProps.pathname.length - 1
240 );
241 }
242 }
243
244 get search(): string {
245 return this._urlProps.search;
246 }
247
248 set search(search: string) {
249 if (!search.startsWith("?")) {
250 this._urlProps.search = "?" + search;
251 } else {
252 this._urlProps.search = search;
253 }
254 }
255
256 get hash(): string {
257 return this._urlProps.hash;
258 }
259
260 set hash(hash: string) {
261 if (!hash.startsWith("#")) {
262 this._urlProps.hash = "#" + hash;
263 } else {
264 this._urlProps.hash = hash;
265 }
266 }
267
268 get host(): string {
269 if (this._urlProps.port.length > 0) {
270 return this.hostname + ":" + this._urlProps.port;
271 }
272
273 return this.hostname;
274 }
275
276 set host(host: string) {
277 if (urlCannotBeBaseUrl(this._urlProps)) {
278 return;
279 }
280
281 if (host.includes(":")) {
282 let splitHost = host.split(":");
283 this._urlProps.hostname = splitHost[0];
284 this._urlProps.port = splitHost[1];
285 } else {
286 this._urlProps.hostname = host;
287 }
288 }
289
290 get origin(): string {
291 return this._urlProps.protocol + "//" + this.host;
292 }
293
294 get href(): string {
295 let href = "";
296
297 // Add the protocol
298 href += this._urlProps.protocol;
299 if (SPECIAL_SCHEMES.includes(this._urlProps.protocol)) {
300 href += "//";
301 }
302
303 // Add the username and password if they exist
304 if (this._urlProps.username.length > 0) {
305 href += this._urlProps.username;
306 if (this._urlProps.password.length > 0) {
307 href += ":" + this._urlProps.password;
308 }
309 href += "@";
310 }
311
312 // Add the host
313 href += this.host;
314
315 // Add the pathname
316 href += this.pathname;
317 if (!SPECIAL_SCHEMES.includes(this._urlProps.protocol)) {
318 // Remove the trailing slash
319 href = href.slice(0, href.length - 1);
320 }
321
322 // Add the search
323 href += this._urlProps.search;
324
325 // Add the hash
326 href += this._urlProps.hash;
327
328 return href;
329 }
330
331 set href(absoluteUrl: string) {
332 URLParser.parseAbsoluteUrl(absoluteUrl, this._urlProps);
333 }
334
335 // Synonyms for href: https://developer.mozilla.org/en-US/docs/Web/API/URL#Methods
336
337 toString(): string {
338 return this.href;
339 }
340
341 toJSON(): string {
342 return this.href;
343 }
344}