Skip to content

Commit ec455e1

Browse files
jasonadenbenlesh
authored andcommittedApr 24, 2019
feat(common): add UrlCodec type for use with upgrade applications (#30055)
This abstract class (and AngularJSUrlCodec) are used for serializing and deserializing pieces of a URL string. AngularJS had a different way of doing this than Angular, and using this class in conjunction with the LocationUpgradeService an application can have control over how AngularJS URLs are serialized and deserialized. PR Close #30055
1 parent 825efa8 commit ec455e1

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed
 

‎packages/common/upgrade/src/params.ts

+261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* A codec for encoding and decoding URL parts.
11+
*
12+
* @publicApi
13+
**/
14+
export abstract class UrlCodec {
15+
abstract encodePath(path: string): string;
16+
abstract decodePath(path: string): string;
17+
18+
abstract encodeSearch(search: string|{[k: string]: unknown}): string;
19+
abstract decodeSearch(search: string): {[k: string]: unknown};
20+
21+
abstract encodeHash(hash: string): string;
22+
abstract decodeHash(hash: string): string;
23+
24+
abstract normalize(href: string): string;
25+
abstract normalize(path: string, search: {[k: string]: unknown}, hash: string, baseUrl?: string):
26+
string;
27+
28+
abstract areEqual(a: string, b: string): boolean;
29+
30+
abstract parse(url: string, base?: string): {
31+
href: string,
32+
protocol: string,
33+
host: string,
34+
search: string,
35+
hash: string,
36+
hostname: string,
37+
port: string,
38+
pathname: string
39+
};
40+
}
41+
42+
/**
43+
* A `AngularJSUrlCodec` that uses logic from AngularJS to serialize and parse URLs
44+
* and URL parameters
45+
*
46+
* @publicApi
47+
*/
48+
export class AngularJSUrlCodec implements UrlCodec {
49+
encodePath(path: string): string {
50+
const segments = path.split('/');
51+
let i = segments.length;
52+
53+
while (i--) {
54+
// decode forward slashes to prevent them from being double encoded
55+
segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/'));
56+
}
57+
58+
path = segments.join('/');
59+
return _stripIndexHtml((path && path[0] !== '/' && '/' || '') + path);
60+
}
61+
62+
encodeSearch(search: string|{[k: string]: unknown}): string {
63+
if (typeof search === 'string') {
64+
search = parseKeyValue(search);
65+
}
66+
67+
search = toKeyValue(search);
68+
return search ? '?' + search : '';
69+
}
70+
71+
encodeHash(hash: string) {
72+
hash = encodeUriSegment(hash);
73+
return hash ? '#' + hash : '';
74+
}
75+
76+
decodePath(path: string, html5Mode = true): string {
77+
const segments = path.split('/');
78+
let i = segments.length;
79+
80+
while (i--) {
81+
segments[i] = decodeURIComponent(segments[i]);
82+
if (html5Mode) {
83+
// encode forward slashes to prevent them from being mistaken for path separators
84+
segments[i] = segments[i].replace(/\//g, '%2F');
85+
}
86+
}
87+
88+
return segments.join('/');
89+
}
90+
91+
decodeSearch(search: string) { return parseKeyValue(search); }
92+
93+
decodeHash(hash: string) {
94+
hash = decodeURIComponent(hash);
95+
return hash[0] === '#' ? hash.substring(1) : hash;
96+
}
97+
98+
normalize(href: string): string;
99+
normalize(path: string, search: {[k: string]: unknown}, hash: string, baseUrl?: string): string;
100+
normalize(pathOrHref: string, search?: {[k: string]: unknown}, hash?: string, baseUrl?: string):
101+
string {
102+
if (arguments.length === 1) {
103+
const parsed = this.parse(pathOrHref, baseUrl);
104+
105+
if (typeof parsed === 'string') {
106+
return parsed;
107+
}
108+
109+
const serverUrl =
110+
`${parsed.protocol}://${parsed.hostname}${parsed.port ? ':' + parsed.port : ''}`;
111+
112+
return this.normalize(
113+
this.decodePath(parsed.pathname), this.decodeSearch(parsed.search),
114+
this.decodeHash(parsed.hash), serverUrl);
115+
} else {
116+
const encPath = this.encodePath(pathOrHref);
117+
const encSearch = search && this.encodeSearch(search) || '';
118+
const encHash = hash && this.encodeHash(hash) || '';
119+
120+
let joinedPath = (baseUrl || '') + encPath;
121+
122+
if (!joinedPath.length || joinedPath[0] !== '/') {
123+
joinedPath = '/' + joinedPath;
124+
}
125+
return joinedPath + encSearch + encHash;
126+
}
127+
}
128+
129+
areEqual(a: string, b: string) { return this.normalize(a) === this.normalize(b); }
130+
131+
parse(url: string, base?: string) {
132+
try {
133+
const parsed = new URL(url, base);
134+
return {
135+
href: parsed.href,
136+
protocol: parsed.protocol ? parsed.protocol.replace(/:$/, '') : '',
137+
host: parsed.host,
138+
search: parsed.search ? parsed.search.replace(/^\?/, '') : '',
139+
hash: parsed.hash ? parsed.hash.replace(/^#/, '') : '',
140+
hostname: parsed.hostname,
141+
port: parsed.port,
142+
pathname: (parsed.pathname.charAt(0) === '/') ? parsed.pathname : '/' + parsed.pathname
143+
};
144+
} catch (e) {
145+
throw new Error(`Invalid URL (${url}) with base (${base})`);
146+
}
147+
}
148+
}
149+
150+
function _stripIndexHtml(url: string): string {
151+
return url.replace(/\/index.html$/, '');
152+
}
153+
154+
/**
155+
* Tries to decode the URI component without throwing an exception.
156+
*
157+
* @private
158+
* @param str value potential URI component to check.
159+
* @returns {boolean} True if `value` can be decoded
160+
* with the decodeURIComponent function.
161+
*/
162+
function tryDecodeURIComponent(value: string) {
163+
try {
164+
return decodeURIComponent(value);
165+
} catch (e) {
166+
// Ignore any invalid uri component.
167+
return undefined;
168+
}
169+
}
170+
171+
172+
/**
173+
* Parses an escaped url query string into key-value pairs.
174+
* @returns {Object.<string,boolean|Array>}
175+
*/
176+
function parseKeyValue(keyValue: string): {[k: string]: unknown} {
177+
const obj: {[k: string]: unknown} = {};
178+
(keyValue || '').split('&').forEach((keyValue) => {
179+
let splitPoint, key, val;
180+
if (keyValue) {
181+
key = keyValue = keyValue.replace(/\+/g, '%20');
182+
splitPoint = keyValue.indexOf('=');
183+
if (splitPoint !== -1) {
184+
key = keyValue.substring(0, splitPoint);
185+
val = keyValue.substring(splitPoint + 1);
186+
}
187+
key = tryDecodeURIComponent(key);
188+
if (typeof key !== 'undefined') {
189+
val = typeof val !== 'undefined' ? tryDecodeURIComponent(val) : true;
190+
if (!obj.hasOwnProperty(key)) {
191+
obj[key] = val;
192+
} else if (Array.isArray(obj[key])) {
193+
(obj[key] as unknown[]).push(val);
194+
} else {
195+
obj[key] = [obj[key], val];
196+
}
197+
}
198+
}
199+
});
200+
return obj;
201+
}
202+
203+
function toKeyValue(obj: {[k: string]: unknown}) {
204+
const parts: unknown[] = [];
205+
for (const key in obj) {
206+
let value = obj[key];
207+
if (Array.isArray(value)) {
208+
value.forEach((arrayValue) => {
209+
parts.push(
210+
encodeUriQuery(key, true) +
211+
(arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true)));
212+
});
213+
} else {
214+
parts.push(
215+
encodeUriQuery(key, true) +
216+
(value === true ? '' : '=' + encodeUriQuery(value as any, true)));
217+
}
218+
}
219+
return parts.length ? parts.join('&') : '';
220+
}
221+
222+
223+
/**
224+
* We need our custom method because encodeURIComponent is too aggressive and doesn't follow
225+
* http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
226+
* segments:
227+
* segment = *pchar
228+
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
229+
* pct-encoded = "%" HEXDIG HEXDIG
230+
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
231+
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
232+
* / "*" / "+" / "," / ";" / "="
233+
*/
234+
function encodeUriSegment(val: string) {
235+
return encodeUriQuery(val, true)
236+
.replace(/%26/gi, '&')
237+
.replace(/%3D/gi, '=')
238+
.replace(/%2B/gi, '+');
239+
}
240+
241+
242+
/**
243+
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
244+
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
245+
* encoded per http://tools.ietf.org/html/rfc3986:
246+
* query = *( pchar / "/" / "?" )
247+
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
248+
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
249+
* pct-encoded = "%" HEXDIG HEXDIG
250+
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
251+
* / "*" / "+" / "," / ";" / "="
252+
*/
253+
function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) {
254+
return encodeURIComponent(val)
255+
.replace(/%40/gi, '@')
256+
.replace(/%3A/gi, ':')
257+
.replace(/%24/g, '$')
258+
.replace(/%2C/gi, ',')
259+
.replace(/%3B/gi, ';')
260+
.replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
261+
}

0 commit comments

Comments
 (0)
Please sign in to comment.