1 |
|
2 |
|
3 | import { SPECIAL_SCHEMES, throwInvalidUrlError } from "./util";
|
4 |
|
5 | import { URLProperties } from "./url-properties";
|
6 |
|
7 | export class URLParser {
|
8 | static isAbsoluteUrl(url: string): boolean {
|
9 | if (url.startsWith("//")) {
|
10 |
|
11 | return false;
|
12 | }
|
13 | if (url.indexOf(":") > 0) {
|
14 | return true;
|
15 | }
|
16 |
|
17 | return false;
|
18 | }
|
19 |
|
20 | static applySchemeOrPathRelativeUrl(
|
21 | relativeUrl: string,
|
22 | urlProps: URLProperties
|
23 | ): void {
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | if (relativeUrl.startsWith("//")) {
|
31 |
|
32 | let urlAfterAuth = URLParser.parseAuth(
|
33 | relativeUrl.substring(2),
|
34 | urlProps
|
35 | );
|
36 | let urlAfterHost = URLParser.parseHost(urlAfterAuth, urlProps);
|
37 | let urlAfterPath = URLParser.parsePath(urlAfterHost, urlProps);
|
38 | let urlAfterSearch = URLParser.parseSearch(urlAfterPath, urlProps);
|
39 | URLParser.parseHash(urlAfterSearch, urlProps);
|
40 | return;
|
41 | }
|
42 |
|
43 |
|
44 | if (relativeUrl.startsWith(".")) {
|
45 |
|
46 | } else {
|
47 | urlProps.pathname = "";
|
48 | urlProps.search = "";
|
49 | urlProps.hash = "";
|
50 | }
|
51 |
|
52 |
|
53 | URLParser.applyPathRelativeUrl(relativeUrl, urlProps);
|
54 | }
|
55 |
|
56 | static applyPathRelativeUrl(
|
57 | relativeUrl: string,
|
58 | urlProps: URLProperties
|
59 | ): void {
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | let appliedRelativeUrl = urlProps.pathname;
|
65 | if (relativeUrl.startsWith("/")) {
|
66 | appliedRelativeUrl = relativeUrl;
|
67 | } else {
|
68 | if (!appliedRelativeUrl.endsWith("/") && !relativeUrl.startsWith("/")) {
|
69 | appliedRelativeUrl += "/" + relativeUrl;
|
70 | } else {
|
71 | appliedRelativeUrl += relativeUrl;
|
72 | }
|
73 | }
|
74 |
|
75 |
|
76 | if (appliedRelativeUrl.endsWith(".")) {
|
77 | appliedRelativeUrl += "/";
|
78 | }
|
79 |
|
80 |
|
81 | while (appliedRelativeUrl.includes("/./")) {
|
82 | appliedRelativeUrl = appliedRelativeUrl.replace("/./", "/");
|
83 | }
|
84 |
|
85 |
|
86 | while (appliedRelativeUrl.includes("/../")) {
|
87 | let parentDirectoryIndex = appliedRelativeUrl.indexOf("../");
|
88 |
|
89 |
|
90 | if (parentDirectoryIndex > 1) {
|
91 |
|
92 | let parentIndex = appliedRelativeUrl.lastIndexOf(
|
93 | "/",
|
94 | parentDirectoryIndex - 2
|
95 | );
|
96 | let parentReplaceTerm = appliedRelativeUrl.slice(
|
97 | parentIndex,
|
98 | parentDirectoryIndex + 3
|
99 | );
|
100 |
|
101 | appliedRelativeUrl = appliedRelativeUrl.replace(parentReplaceTerm, "/");
|
102 | } else {
|
103 | throw new Error(
|
104 | "Relative url " +
|
105 | relativeUrl +
|
106 | " cannot be applied to the url " +
|
107 | urlProps.toString()
|
108 | );
|
109 | }
|
110 | }
|
111 |
|
112 | relativeUrl = appliedRelativeUrl;
|
113 | if (!relativeUrl.startsWith("/")) {
|
114 | relativeUrl = "/" + relativeUrl;
|
115 | }
|
116 |
|
117 |
|
118 |
|
119 | let urlAfterHost = relativeUrl + urlProps.search + urlProps.hash;
|
120 | let urlAfterPath = URLParser.parsePath(urlAfterHost, urlProps);
|
121 | let urlAfterSearch = URLParser.parseSearch(urlAfterPath, urlProps);
|
122 | URLParser.parseHash(urlAfterSearch, urlProps);
|
123 |
|
124 |
|
125 | if (relativeUrl.endsWith("/") && !urlProps.pathname.endsWith("/")) {
|
126 | urlProps.pathname += "/";
|
127 | }
|
128 |
|
129 | return;
|
130 | }
|
131 |
|
132 | static parseAbsoluteUrl(absoluteUrl: string, urlProps: URLProperties): void {
|
133 |
|
134 | if (absoluteUrl.startsWith("file:")) {
|
135 | absoluteUrl = absoluteUrl.replaceAll("|", ":");
|
136 | }
|
137 |
|
138 |
|
139 | let urlAfterProtocol = URLParser.parseProtocol(absoluteUrl, urlProps);
|
140 | let urlAfterAuth = URLParser.parseAuth(urlAfterProtocol, urlProps);
|
141 | let urlAfterHost = URLParser.parseHost(urlAfterAuth, urlProps);
|
142 |
|
143 |
|
144 | if (urlAfterHost.length > 0) {
|
145 | urlProps.pathname = "";
|
146 | urlProps.search = "";
|
147 | urlProps.hash = "";
|
148 |
|
149 |
|
150 | while (urlAfterHost.startsWith("/../")) {
|
151 | urlAfterHost = urlAfterHost.replace("/../", "/");
|
152 | }
|
153 |
|
154 | URLParser.applyPathRelativeUrl(urlAfterHost, urlProps);
|
155 | }
|
156 | }
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | static parseProtocol(absoluteUrl: string, urlProps: URLProperties): string {
|
162 |
|
163 | let protocolIndex = absoluteUrl.indexOf(":");
|
164 |
|
165 |
|
166 | if (protocolIndex > -1) {
|
167 | urlProps.protocol = absoluteUrl.substring(0, protocolIndex + 1);
|
168 |
|
169 |
|
170 |
|
171 | if (urlProps.protocol == "file:") {
|
172 | let absoluteUrlNoProtocol = absoluteUrl.replace(
|
173 | urlProps.protocol + "//",
|
174 | ""
|
175 | );
|
176 |
|
177 | if (absoluteUrlNoProtocol.indexOf("/") > -1) {
|
178 | return absoluteUrlNoProtocol.substring(
|
179 | absoluteUrlNoProtocol.indexOf("/")
|
180 | );
|
181 | } else {
|
182 | return "";
|
183 | }
|
184 | }
|
185 |
|
186 | let protocolEndIndex = protocolIndex + 1;
|
187 | while (
|
188 | absoluteUrl.charAt(protocolEndIndex) == "/" &&
|
189 | protocolEndIndex < absoluteUrl.length - 1
|
190 | ) {
|
191 | protocolEndIndex++;
|
192 | }
|
193 |
|
194 | return absoluteUrl.substring(protocolEndIndex);
|
195 | }
|
196 |
|
197 |
|
198 | return absoluteUrl;
|
199 | }
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | static parseAuth(urlAfterProtocol: string, urlProps: URLProperties): string {
|
205 |
|
206 | let authIndex = urlAfterProtocol.indexOf("@");
|
207 |
|
208 |
|
209 |
|
210 | let pathIndex = urlAfterProtocol.indexOf("/");
|
211 |
|
212 | if (authIndex > 0 && (pathIndex == -1 || authIndex < pathIndex)) {
|
213 | let auth = urlAfterProtocol.substring(0, authIndex);
|
214 |
|
215 | if (auth.includes(":")) {
|
216 | let authSplit = auth.split(":");
|
217 | urlProps.username = authSplit[0];
|
218 | urlProps.password = authSplit[1];
|
219 | } else {
|
220 | urlProps.username = auth;
|
221 | }
|
222 |
|
223 |
|
224 | return urlAfterProtocol.substring(auth.length + 1);
|
225 | }
|
226 |
|
227 |
|
228 | return urlAfterProtocol;
|
229 | }
|
230 |
|
231 |
|
232 |
|
233 |
|
234 | static parseHost(urlAfterAuth: string, urlProps: URLProperties): string {
|
235 |
|
236 | let urlAfterHost = "";
|
237 |
|
238 |
|
239 |
|
240 | let hostnameAndPort = "";
|
241 | let pathIndex = urlAfterAuth.indexOf("/");
|
242 | let searchIndex = urlAfterAuth.indexOf("?");
|
243 | let hashIndex = urlAfterAuth.indexOf("#");
|
244 | if (pathIndex > -1) {
|
245 | hostnameAndPort = urlAfterAuth.substring(0, pathIndex);
|
246 | urlAfterHost = urlAfterAuth.substring(pathIndex);
|
247 | } else if (searchIndex > -1) {
|
248 | hostnameAndPort = urlAfterAuth.substring(0, searchIndex);
|
249 | urlAfterHost = urlAfterAuth.substring(searchIndex);
|
250 | } else if (hashIndex > -1) {
|
251 | hostnameAndPort = urlAfterAuth.substring(0, hashIndex);
|
252 | urlAfterHost = urlAfterAuth.substring(hashIndex);
|
253 | } else {
|
254 | hostnameAndPort = urlAfterAuth;
|
255 | urlAfterHost = "";
|
256 | }
|
257 |
|
258 | let hostname = "";
|
259 | let port = "";
|
260 |
|
261 | if (hostnameAndPort.includes("[")) {
|
262 |
|
263 |
|
264 |
|
265 | if (!hostnameAndPort.startsWith("[") || !hostnameAndPort.endsWith("]")) {
|
266 | throwInvalidUrlError();
|
267 | }
|
268 |
|
269 | let splitAddress = hostnameAndPort.split(":");
|
270 | if (splitAddress.length != 8) {
|
271 | throwInvalidUrlError();
|
272 | }
|
273 |
|
274 | hostname = hostnameAndPort;
|
275 | } else if (hostnameAndPort.includes(":")) {
|
276 | let hostnameAndPortSplit = hostnameAndPort.split(":");
|
277 | hostname = hostnameAndPortSplit[0];
|
278 |
|
279 |
|
280 | let portOrNaN = F32.parseInt(hostnameAndPortSplit[1], 10);
|
281 | if (isNaN(portOrNaN) || portOrNaN <= 0 || portOrNaN >= 65536) {
|
282 | throwInvalidUrlError();
|
283 | }
|
284 |
|
285 | port = I32.parseInt(hostnameAndPortSplit[1], 10).toString();
|
286 | } else {
|
287 | hostname = hostnameAndPort;
|
288 | }
|
289 |
|
290 |
|
291 |
|
292 | if (
|
293 | port.length > 0 &&
|
294 | ((urlProps.protocol == "ftp:" && port == "22") ||
|
295 | (urlProps.protocol == "http:" && port == "80") ||
|
296 | (urlProps.protocol == "https:" && port == "443") ||
|
297 | (urlProps.protocol == "ws:" && port == "80") ||
|
298 | (urlProps.protocol == "wss:" && port == "443"))
|
299 | ) {
|
300 | port = "";
|
301 | }
|
302 |
|
303 | urlProps.hostname = hostname;
|
304 | urlProps.port = port;
|
305 |
|
306 |
|
307 | return urlAfterHost;
|
308 | }
|
309 |
|
310 |
|
311 |
|
312 |
|
313 | static parsePath(urlAfterHost: string, urlProps: URLProperties): string {
|
314 | if (urlAfterHost.length == 0) {
|
315 | return "";
|
316 | }
|
317 |
|
318 | let pathIndex = urlAfterHost.indexOf("/");
|
319 | let searchIndex = urlAfterHost.indexOf("?");
|
320 | let hashIndex = urlAfterHost.indexOf("#");
|
321 |
|
322 | if (pathIndex > -1) {
|
323 |
|
324 | if (searchIndex > -1) {
|
325 | urlProps.pathname = urlAfterHost.substring(0, searchIndex);
|
326 | } else if (hashIndex > -1) {
|
327 | urlProps.pathname = urlAfterHost.substring(0, hashIndex);
|
328 | } else {
|
329 | urlProps.pathname = urlAfterHost;
|
330 | }
|
331 |
|
332 |
|
333 | if (
|
334 | urlProps.pathname.endsWith("/") &&
|
335 | !urlProps.pathname.endsWith("//")
|
336 | ) {
|
337 | urlProps.pathname = urlProps.pathname.slice(
|
338 | 0,
|
339 | urlProps.pathname.length - 1
|
340 | );
|
341 | }
|
342 |
|
343 |
|
344 | if (searchIndex > -1) {
|
345 | return urlAfterHost.substring(searchIndex);
|
346 | } else if (hashIndex > -1) {
|
347 | return urlAfterHost.substring(hashIndex);
|
348 | } else {
|
349 | return "";
|
350 | }
|
351 | }
|
352 |
|
353 |
|
354 | return urlAfterHost;
|
355 | }
|
356 |
|
357 |
|
358 |
|
359 |
|
360 | static parseSearch(urlAfterPath: string, urlProps: URLProperties): string {
|
361 | if (urlAfterPath.length == 0) {
|
362 | return "";
|
363 | }
|
364 |
|
365 | let searchIndex = urlAfterPath.indexOf("?");
|
366 | let hashIndex = urlAfterPath.indexOf("#");
|
367 | if (searchIndex > -1) {
|
368 | if (hashIndex > -1) {
|
369 | urlProps.search = urlAfterPath.substring(0, hashIndex);
|
370 | return urlAfterPath.substring(hashIndex);
|
371 | } else {
|
372 | urlProps.search = urlAfterPath;
|
373 | return "";
|
374 | }
|
375 | }
|
376 |
|
377 |
|
378 | return urlAfterPath;
|
379 | }
|
380 |
|
381 |
|
382 |
|
383 | static parseHash(urlAfterSearch: string, urlProps: URLProperties): void {
|
384 | let hashIndex = urlAfterSearch.indexOf("#");
|
385 | if (urlAfterSearch.length > 0 && hashIndex > -1) {
|
386 | urlProps.hash = urlAfterSearch.substring(hashIndex);
|
387 | }
|
388 | }
|
389 |
|
390 | static validateUrl(urlProps: URLProperties): void {
|
391 |
|
392 |
|
393 | if (urlProps.hostname.includes(".")) {
|
394 |
|
395 |
|
396 | if (!SPECIAL_SCHEMES.includes(urlProps.protocol)) {
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 | }
|
404 | } else if (urlProps.hostname == "") {
|
405 |
|
406 |
|
407 | if (
|
408 | SPECIAL_SCHEMES.includes(urlProps.protocol) &&
|
409 | urlProps.protocol != "file:"
|
410 | ) {
|
411 | throwInvalidUrlError();
|
412 | }
|
413 | } else {
|
414 |
|
415 | if (urlProps.hostname.includes("[")) {
|
416 |
|
417 | } else if (
|
418 | SPECIAL_SCHEMES.includes(urlProps.protocol) &&
|
419 | urlProps.protocol != "http:" &&
|
420 | urlProps.protocol != "https:"
|
421 | ) {
|
422 |
|
423 |
|
424 |
|
425 | throwInvalidUrlError();
|
426 | }
|
427 | }
|
428 |
|
429 |
|
430 | }
|
431 | }
|