1 |
|
2 |
|
3 |
|
4 | import { replaceAll } from "./util/utils";
|
5 |
|
6 | type URLQueryParseState = "ParameterName" | "ParameterValue" | "Invalid";
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | export class URLQuery {
|
12 | private readonly _rawQuery: { [queryParameterName: string]: string | string[] } = {};
|
13 |
|
14 | |
15 |
|
16 |
|
17 | public any(): boolean {
|
18 | return Object.keys(this._rawQuery).length > 0;
|
19 | }
|
20 |
|
21 | |
22 |
|
23 |
|
24 |
|
25 |
|
26 | public set(parameterName: string, parameterValue: any): void {
|
27 | if (parameterName) {
|
28 | if (parameterValue != undefined) {
|
29 | const newValue = Array.isArray(parameterValue) ? parameterValue : parameterValue.toString();
|
30 | this._rawQuery[parameterName] = newValue;
|
31 | } else {
|
32 | delete this._rawQuery[parameterName];
|
33 | }
|
34 | }
|
35 | }
|
36 |
|
37 | |
38 |
|
39 |
|
40 |
|
41 | public get(parameterName: string): string | string[] | undefined {
|
42 | return parameterName ? this._rawQuery[parameterName] : undefined;
|
43 | }
|
44 |
|
45 | |
46 |
|
47 |
|
48 | public toString(): string {
|
49 | let result = "";
|
50 | for (const parameterName in this._rawQuery) {
|
51 | if (result) {
|
52 | result += "&";
|
53 | }
|
54 | const parameterValue = this._rawQuery[parameterName];
|
55 | if (Array.isArray(parameterValue)) {
|
56 | const parameterStrings = [];
|
57 | for (const parameterValueElement of parameterValue) {
|
58 | parameterStrings.push(`${parameterName}=${parameterValueElement}`);
|
59 | }
|
60 | result += parameterStrings.join("&");
|
61 | } else {
|
62 | result += `${parameterName}=${parameterValue}`;
|
63 | }
|
64 | }
|
65 | return result;
|
66 | }
|
67 |
|
68 | |
69 |
|
70 |
|
71 | public static parse(text: string): URLQuery {
|
72 | const result = new URLQuery();
|
73 |
|
74 | if (text) {
|
75 | if (text.startsWith("?")) {
|
76 | text = text.substring(1);
|
77 | }
|
78 |
|
79 | let currentState: URLQueryParseState = "ParameterName";
|
80 |
|
81 | let parameterName = "";
|
82 | let parameterValue = "";
|
83 | for (let i = 0; i < text.length; ++i) {
|
84 | const currentCharacter: string = text[i];
|
85 | switch (currentState) {
|
86 | case "ParameterName":
|
87 | switch (currentCharacter) {
|
88 | case "=":
|
89 | currentState = "ParameterValue";
|
90 | break;
|
91 |
|
92 | case "&":
|
93 | parameterName = "";
|
94 | parameterValue = "";
|
95 | break;
|
96 |
|
97 | default:
|
98 | parameterName += currentCharacter;
|
99 | break;
|
100 | }
|
101 | break;
|
102 |
|
103 | case "ParameterValue":
|
104 | switch (currentCharacter) {
|
105 | case "&":
|
106 | result.set(parameterName, parameterValue);
|
107 | parameterName = "";
|
108 | parameterValue = "";
|
109 | currentState = "ParameterName";
|
110 | break;
|
111 |
|
112 | default:
|
113 | parameterValue += currentCharacter;
|
114 | break;
|
115 | }
|
116 | break;
|
117 |
|
118 | default:
|
119 | throw new Error("Unrecognized URLQuery parse state: " + currentState);
|
120 | }
|
121 | }
|
122 | if (currentState === "ParameterValue") {
|
123 | result.set(parameterName, parameterValue);
|
124 | }
|
125 | }
|
126 |
|
127 | return result;
|
128 | }
|
129 | }
|
130 |
|
131 |
|
132 |
|
133 |
|
134 | export class URLBuilder {
|
135 | private _scheme: string | undefined;
|
136 | private _host: string | undefined;
|
137 | private _port: string | undefined;
|
138 | private _path: string | undefined;
|
139 | private _query: URLQuery | undefined;
|
140 |
|
141 | |
142 |
|
143 |
|
144 |
|
145 | public setScheme(scheme: string | undefined): void {
|
146 | if (!scheme) {
|
147 | this._scheme = undefined;
|
148 | } else {
|
149 | this.set(scheme, "SCHEME");
|
150 | }
|
151 | }
|
152 |
|
153 | |
154 |
|
155 |
|
156 | public getScheme(): string | undefined {
|
157 | return this._scheme;
|
158 | }
|
159 |
|
160 | |
161 |
|
162 |
|
163 |
|
164 | public setHost(host: string | undefined): void {
|
165 | if (!host) {
|
166 | this._host = undefined;
|
167 | } else {
|
168 | this.set(host, "SCHEME_OR_HOST");
|
169 | }
|
170 | }
|
171 |
|
172 | |
173 |
|
174 |
|
175 | public getHost(): string | undefined {
|
176 | return this._host;
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 | public setPort(port: number | string | undefined): void {
|
184 | if (port == undefined || port === "") {
|
185 | this._port = undefined;
|
186 | } else {
|
187 | this.set(port.toString(), "PORT");
|
188 | }
|
189 | }
|
190 |
|
191 | |
192 |
|
193 |
|
194 | public getPort(): string | undefined {
|
195 | return this._port;
|
196 | }
|
197 |
|
198 | |
199 |
|
200 |
|
201 |
|
202 | public setPath(path: string | undefined): void {
|
203 | if (!path) {
|
204 | this._path = undefined;
|
205 | } else {
|
206 | const schemeIndex = path.indexOf("://");
|
207 | if (schemeIndex !== -1) {
|
208 | const schemeStart = path.lastIndexOf("/", schemeIndex);
|
209 |
|
210 |
|
211 | this.set(schemeStart === -1 ? path : path.substr(schemeStart + 1), "SCHEME");
|
212 | } else {
|
213 | this.set(path, "PATH");
|
214 | }
|
215 | }
|
216 | }
|
217 |
|
218 | |
219 |
|
220 |
|
221 |
|
222 | public appendPath(path: string | undefined): void {
|
223 | if (path) {
|
224 | let currentPath: string | undefined = this.getPath();
|
225 | if (currentPath) {
|
226 | if (!currentPath.endsWith("/")) {
|
227 | currentPath += "/";
|
228 | }
|
229 |
|
230 | if (path.startsWith("/")) {
|
231 | path = path.substring(1);
|
232 | }
|
233 |
|
234 | path = currentPath + path;
|
235 | }
|
236 | this.set(path, "PATH");
|
237 | }
|
238 | }
|
239 |
|
240 | |
241 |
|
242 |
|
243 | public getPath(): string | undefined {
|
244 | return this._path;
|
245 | }
|
246 |
|
247 | |
248 |
|
249 |
|
250 | public setQuery(query: string | undefined): void {
|
251 | if (!query) {
|
252 | this._query = undefined;
|
253 | } else {
|
254 | this._query = URLQuery.parse(query);
|
255 | }
|
256 | }
|
257 |
|
258 | |
259 |
|
260 |
|
261 |
|
262 |
|
263 | public setQueryParameter(queryParameterName: string, queryParameterValue: any): void {
|
264 | if (queryParameterName) {
|
265 | if (!this._query) {
|
266 | this._query = new URLQuery();
|
267 | }
|
268 | this._query.set(queryParameterName, queryParameterValue);
|
269 | }
|
270 | }
|
271 |
|
272 | |
273 |
|
274 |
|
275 |
|
276 | public getQueryParameterValue(queryParameterName: string): string | string[] | undefined {
|
277 | return this._query ? this._query.get(queryParameterName) : undefined;
|
278 | }
|
279 |
|
280 | |
281 |
|
282 |
|
283 | public getQuery(): string | undefined {
|
284 | return this._query ? this._query.toString() : undefined;
|
285 | }
|
286 |
|
287 | |
288 |
|
289 |
|
290 | private set(text: string, startState: URLTokenizerState): void {
|
291 | const tokenizer = new URLTokenizer(text, startState);
|
292 |
|
293 | while (tokenizer.next()) {
|
294 | const token: URLToken | undefined = tokenizer.current();
|
295 | if (token) {
|
296 | switch (token.type) {
|
297 | case "SCHEME":
|
298 | this._scheme = token.text || undefined;
|
299 | break;
|
300 |
|
301 | case "HOST":
|
302 | this._host = token.text || undefined;
|
303 | break;
|
304 |
|
305 | case "PORT":
|
306 | this._port = token.text || undefined;
|
307 | break;
|
308 |
|
309 | case "PATH":
|
310 | const tokenPath: string | undefined = token.text || undefined;
|
311 | if (!this._path || this._path === "/" || tokenPath !== "/") {
|
312 | this._path = tokenPath;
|
313 | }
|
314 | break;
|
315 |
|
316 | case "QUERY":
|
317 | this._query = URLQuery.parse(token.text);
|
318 | break;
|
319 |
|
320 | default:
|
321 | throw new Error(`Unrecognized URLTokenType: ${token.type}`);
|
322 | }
|
323 | }
|
324 | }
|
325 | }
|
326 |
|
327 | public toString(): string {
|
328 | let result = "";
|
329 |
|
330 | if (this._scheme) {
|
331 | result += `${this._scheme}://`;
|
332 | }
|
333 |
|
334 | if (this._host) {
|
335 | result += this._host;
|
336 | }
|
337 |
|
338 | if (this._port) {
|
339 | result += `:${this._port}`;
|
340 | }
|
341 |
|
342 | if (this._path) {
|
343 | if (!this._path.startsWith("/")) {
|
344 | result += "/";
|
345 | }
|
346 | result += this._path;
|
347 | }
|
348 |
|
349 | if (this._query && this._query.any()) {
|
350 | result += `?${this._query.toString()}`;
|
351 | }
|
352 |
|
353 | return result;
|
354 | }
|
355 |
|
356 | |
357 |
|
358 |
|
359 |
|
360 | public replaceAll(searchValue: string, replaceValue: string): void {
|
361 | if (searchValue) {
|
362 | this.setScheme(replaceAll(this.getScheme(), searchValue, replaceValue));
|
363 | this.setHost(replaceAll(this.getHost(), searchValue, replaceValue));
|
364 | this.setPort(replaceAll(this.getPort(), searchValue, replaceValue));
|
365 | this.setPath(replaceAll(this.getPath(), searchValue, replaceValue));
|
366 | this.setQuery(replaceAll(this.getQuery(), searchValue, replaceValue));
|
367 | }
|
368 | }
|
369 |
|
370 | public static parse(text: string): URLBuilder {
|
371 | const result = new URLBuilder();
|
372 | result.set(text, "SCHEME_OR_HOST");
|
373 | return result;
|
374 | }
|
375 | }
|
376 |
|
377 | type URLTokenizerState = "SCHEME" | "SCHEME_OR_HOST" | "HOST" | "PORT" | "PATH" | "QUERY" | "DONE";
|
378 |
|
379 | type URLTokenType = "SCHEME" | "HOST" | "PORT" | "PATH" | "QUERY";
|
380 |
|
381 | export class URLToken {
|
382 | public constructor(public readonly text: string, public readonly type: URLTokenType) {}
|
383 |
|
384 | public static scheme(text: string): URLToken {
|
385 | return new URLToken(text, "SCHEME");
|
386 | }
|
387 |
|
388 | public static host(text: string): URLToken {
|
389 | return new URLToken(text, "HOST");
|
390 | }
|
391 |
|
392 | public static port(text: string): URLToken {
|
393 | return new URLToken(text, "PORT");
|
394 | }
|
395 |
|
396 | public static path(text: string): URLToken {
|
397 | return new URLToken(text, "PATH");
|
398 | }
|
399 |
|
400 | public static query(text: string): URLToken {
|
401 | return new URLToken(text, "QUERY");
|
402 | }
|
403 | }
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 | export function isAlphaNumericCharacter(character: string): boolean {
|
410 | const characterCode: number = character.charCodeAt(0);
|
411 | return (
|
412 | (48 <= characterCode && characterCode <= 57) ||
|
413 | (65 <= characterCode && characterCode <= 90) ||
|
414 | (97 <= characterCode && characterCode <= 122)
|
415 | );
|
416 | }
|
417 |
|
418 |
|
419 |
|
420 |
|
421 | export class URLTokenizer {
|
422 | readonly _textLength: number;
|
423 | _currentState: URLTokenizerState;
|
424 | _currentIndex: number;
|
425 | _currentToken: URLToken | undefined;
|
426 |
|
427 | public constructor(readonly _text: string, state?: URLTokenizerState) {
|
428 | this._textLength = _text ? _text.length : 0;
|
429 | this._currentState = state != undefined ? state : "SCHEME_OR_HOST";
|
430 | this._currentIndex = 0;
|
431 | }
|
432 |
|
433 | |
434 |
|
435 |
|
436 |
|
437 | public current(): URLToken | undefined {
|
438 | return this._currentToken;
|
439 | }
|
440 |
|
441 | |
442 |
|
443 |
|
444 | public next(): boolean {
|
445 | if (!hasCurrentCharacter(this)) {
|
446 | this._currentToken = undefined;
|
447 | } else {
|
448 | switch (this._currentState) {
|
449 | case "SCHEME":
|
450 | nextScheme(this);
|
451 | break;
|
452 |
|
453 | case "SCHEME_OR_HOST":
|
454 | nextSchemeOrHost(this);
|
455 | break;
|
456 |
|
457 | case "HOST":
|
458 | nextHost(this);
|
459 | break;
|
460 |
|
461 | case "PORT":
|
462 | nextPort(this);
|
463 | break;
|
464 |
|
465 | case "PATH":
|
466 | nextPath(this);
|
467 | break;
|
468 |
|
469 | case "QUERY":
|
470 | nextQuery(this);
|
471 | break;
|
472 |
|
473 | default:
|
474 | throw new Error(`Unrecognized URLTokenizerState: ${this._currentState}`);
|
475 | }
|
476 | }
|
477 | return !!this._currentToken;
|
478 | }
|
479 | }
|
480 |
|
481 |
|
482 |
|
483 |
|
484 | function readRemaining(tokenizer: URLTokenizer): string {
|
485 | let result = "";
|
486 | if (tokenizer._currentIndex < tokenizer._textLength) {
|
487 | result = tokenizer._text.substring(tokenizer._currentIndex);
|
488 | tokenizer._currentIndex = tokenizer._textLength;
|
489 | }
|
490 | return result;
|
491 | }
|
492 |
|
493 |
|
494 |
|
495 |
|
496 | function hasCurrentCharacter(tokenizer: URLTokenizer): boolean {
|
497 | return tokenizer._currentIndex < tokenizer._textLength;
|
498 | }
|
499 |
|
500 |
|
501 |
|
502 |
|
503 | function getCurrentCharacter(tokenizer: URLTokenizer): string {
|
504 | return tokenizer._text[tokenizer._currentIndex];
|
505 | }
|
506 |
|
507 |
|
508 |
|
509 |
|
510 |
|
511 | function nextCharacter(tokenizer: URLTokenizer, step?: number): void {
|
512 | if (hasCurrentCharacter(tokenizer)) {
|
513 | if (!step) {
|
514 | step = 1;
|
515 | }
|
516 | tokenizer._currentIndex += step;
|
517 | }
|
518 | }
|
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
524 | function peekCharacters(tokenizer: URLTokenizer, charactersToPeek: number): string {
|
525 | let endIndex: number = tokenizer._currentIndex + charactersToPeek;
|
526 | if (tokenizer._textLength < endIndex) {
|
527 | endIndex = tokenizer._textLength;
|
528 | }
|
529 | return tokenizer._text.substring(tokenizer._currentIndex, endIndex);
|
530 | }
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 | function readWhile(tokenizer: URLTokenizer, condition: (character: string) => boolean): string {
|
537 | let result = "";
|
538 |
|
539 | while (hasCurrentCharacter(tokenizer)) {
|
540 | const currentCharacter: string = getCurrentCharacter(tokenizer);
|
541 | if (!condition(currentCharacter)) {
|
542 | break;
|
543 | } else {
|
544 | result += currentCharacter;
|
545 | nextCharacter(tokenizer);
|
546 | }
|
547 | }
|
548 |
|
549 | return result;
|
550 | }
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 | function readWhileLetterOrDigit(tokenizer: URLTokenizer): string {
|
557 | return readWhile(tokenizer, (character: string) => isAlphaNumericCharacter(character));
|
558 | }
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 | function readUntilCharacter(tokenizer: URLTokenizer, ...terminatingCharacters: string[]): string {
|
565 | return readWhile(
|
566 | tokenizer,
|
567 | (character: string) => terminatingCharacters.indexOf(character) === -1
|
568 | );
|
569 | }
|
570 |
|
571 | function nextScheme(tokenizer: URLTokenizer): void {
|
572 | const scheme: string = readWhileLetterOrDigit(tokenizer);
|
573 | tokenizer._currentToken = URLToken.scheme(scheme);
|
574 | if (!hasCurrentCharacter(tokenizer)) {
|
575 | tokenizer._currentState = "DONE";
|
576 | } else {
|
577 | tokenizer._currentState = "HOST";
|
578 | }
|
579 | }
|
580 |
|
581 | function nextSchemeOrHost(tokenizer: URLTokenizer): void {
|
582 | const schemeOrHost: string = readUntilCharacter(tokenizer, ":", "/", "?");
|
583 | if (!hasCurrentCharacter(tokenizer)) {
|
584 | tokenizer._currentToken = URLToken.host(schemeOrHost);
|
585 | tokenizer._currentState = "DONE";
|
586 | } else if (getCurrentCharacter(tokenizer) === ":") {
|
587 | if (peekCharacters(tokenizer, 3) === "://") {
|
588 | tokenizer._currentToken = URLToken.scheme(schemeOrHost);
|
589 | tokenizer._currentState = "HOST";
|
590 | } else {
|
591 | tokenizer._currentToken = URLToken.host(schemeOrHost);
|
592 | tokenizer._currentState = "PORT";
|
593 | }
|
594 | } else {
|
595 | tokenizer._currentToken = URLToken.host(schemeOrHost);
|
596 | if (getCurrentCharacter(tokenizer) === "/") {
|
597 | tokenizer._currentState = "PATH";
|
598 | } else {
|
599 | tokenizer._currentState = "QUERY";
|
600 | }
|
601 | }
|
602 | }
|
603 |
|
604 | function nextHost(tokenizer: URLTokenizer): void {
|
605 | if (peekCharacters(tokenizer, 3) === "://") {
|
606 | nextCharacter(tokenizer, 3);
|
607 | }
|
608 |
|
609 | const host: string = readUntilCharacter(tokenizer, ":", "/", "?");
|
610 | tokenizer._currentToken = URLToken.host(host);
|
611 |
|
612 | if (!hasCurrentCharacter(tokenizer)) {
|
613 | tokenizer._currentState = "DONE";
|
614 | } else if (getCurrentCharacter(tokenizer) === ":") {
|
615 | tokenizer._currentState = "PORT";
|
616 | } else if (getCurrentCharacter(tokenizer) === "/") {
|
617 | tokenizer._currentState = "PATH";
|
618 | } else {
|
619 | tokenizer._currentState = "QUERY";
|
620 | }
|
621 | }
|
622 |
|
623 | function nextPort(tokenizer: URLTokenizer): void {
|
624 | if (getCurrentCharacter(tokenizer) === ":") {
|
625 | nextCharacter(tokenizer);
|
626 | }
|
627 |
|
628 | const port: string = readUntilCharacter(tokenizer, "/", "?");
|
629 | tokenizer._currentToken = URLToken.port(port);
|
630 |
|
631 | if (!hasCurrentCharacter(tokenizer)) {
|
632 | tokenizer._currentState = "DONE";
|
633 | } else if (getCurrentCharacter(tokenizer) === "/") {
|
634 | tokenizer._currentState = "PATH";
|
635 | } else {
|
636 | tokenizer._currentState = "QUERY";
|
637 | }
|
638 | }
|
639 |
|
640 | function nextPath(tokenizer: URLTokenizer): void {
|
641 | const path: string = readUntilCharacter(tokenizer, "?");
|
642 | tokenizer._currentToken = URLToken.path(path);
|
643 |
|
644 | if (!hasCurrentCharacter(tokenizer)) {
|
645 | tokenizer._currentState = "DONE";
|
646 | } else {
|
647 | tokenizer._currentState = "QUERY";
|
648 | }
|
649 | }
|
650 |
|
651 | function nextQuery(tokenizer: URLTokenizer): void {
|
652 | if (getCurrentCharacter(tokenizer) === "?") {
|
653 | nextCharacter(tokenizer);
|
654 | }
|
655 |
|
656 | const query: string = readRemaining(tokenizer);
|
657 | tokenizer._currentToken = URLToken.query(query);
|
658 | tokenizer._currentState = "DONE";
|
659 | }
|