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 ( / % 2 F / 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 ( / \/ i n d e x .h t m l $ / , '' ) ;
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 ( / % 2 6 / gi, '&' )
237
+ . replace ( / % 3 D / gi, '=' )
238
+ . replace ( / % 2 B / 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 ( / % 4 0 / gi, '@' )
256
+ . replace ( / % 3 A / gi, ':' )
257
+ . replace ( / % 2 4 / g, '$' )
258
+ . replace ( / % 2 C / gi, ',' )
259
+ . replace ( / % 3 B / gi, ';' )
260
+ . replace ( / % 2 0 / g, ( pctEncodeSpaces ? '%20' : '+' ) ) ;
261
+ }
0 commit comments