fix(router): fix URL serialization so special characters are only encoded where needed (#22337)
Fixes: #10280 This change brings Angular largely in line with how AngularJS previously serialized URLs. This is based on [RFC 3986](http://tools.ietf.org/html/rfc3986) and resolves issues such as the above #10280 where URLs could be parsed, re-serialized, then parsed again producing a different result on the second parsing. Adjustments to be aware of in this commit: * Query strings will now serialize with decoded slash (`/`) and question mark (`?`) * URI fragments will now serialize the same as query strings, but hash sign (`#`) will also appear decoded * In the URI path or segments (portion prior to query string and/or fragment), the plus sign (`+`) and ampersand (`&`) will appear decoded * In the URL path or segments, parentheses values (`(` and `)`) will now appear percent encoded as `%28` and `%29` respectively * In the URL path or segments, semicolons will be encoded in their percent encoding `%3B` NOTE: Parentheses and semicolons denoting auxillary routes or matrix params will still appear in their decoded form -- only parentheses and semicolons used as values in a segment or key/value pair for matrix params will be encoded. While these changes are not considered breaking because applications should be decoding URLs and key/value pairs, it is possible that some unit tests will break if comparing hard-coded URLs in tests since that hard coded string will represent the old encoding. Therefore we are releasing this fix in the upcoming Angular v6 rather than adding it to a patch for v5. PR Close #22337
This commit is contained in:

committed by
Victor Berchet

parent
3a809cb431
commit
094666da17
@ -280,7 +280,8 @@ export class DefaultUrlSerializer implements UrlSerializer {
|
||||
serialize(tree: UrlTree): string {
|
||||
const segment = `/${serializeSegment(tree.root, true)}`;
|
||||
const query = serializeQueryParams(tree.queryParams);
|
||||
const fragment = typeof tree.fragment === `string` ? `#${encodeURI(tree.fragment !)}` : '';
|
||||
const fragment =
|
||||
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment !)}` : '';
|
||||
|
||||
return `${segment}${query}${fragment}`;
|
||||
}
|
||||
@ -326,9 +327,10 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
|
||||
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
|
||||
* encoded per http://tools.ietf.org/html/rfc3986:
|
||||
* Encodes a URI string with the default encoding. This function will only ever be called from
|
||||
* `encodeUriQuery` or `encodeUriSegment` as it's the base set of encodings to be used. We need
|
||||
* a custom encoding because encodeURIComponent is too aggressive and encodes stuff that doesn't
|
||||
* have to be encoded per http://tools.ietf.org/html/rfc3986:
|
||||
* query = *( pchar / "/" / "?" )
|
||||
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||
@ -336,13 +338,47 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||
* / "*" / "+" / "," / ";" / "="
|
||||
*/
|
||||
export function encode(s: string): string {
|
||||
function encodeUriString(s: string): string {
|
||||
return encodeURIComponent(s)
|
||||
.replace(/%40/g, '@')
|
||||
.replace(/%3A/gi, ':')
|
||||
.replace(/%24/g, '$')
|
||||
.replace(/%2C/gi, ',')
|
||||
.replace(/%3B/gi, ';');
|
||||
.replace(/%2C/gi, ',');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be used to encode both keys and values in a query string key/value. In
|
||||
* the following URL, you need to call encodeUriQuery on "k" and "v":
|
||||
*
|
||||
* http://www.site.org/html;mk=mv?k=v#f
|
||||
*/
|
||||
export function encodeUriQuery(s: string): string {
|
||||
return encodeUriString(s).replace(/%2F/gi, '/').replace(/%3F/gi, '?').replace(/%3B/gi, ';');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be run on any URI fragment. In the following URL, you need to call
|
||||
* encodeUriSegment on "f":
|
||||
*
|
||||
* http://www.site.org/html;mk=mv?k=v#f
|
||||
*/
|
||||
export function encodeUriFragment(s: string): string {
|
||||
return encodeUriQuery(s).replace(/%23/g, '#');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be run on any URI segment as well as the key and value in a key/value
|
||||
* pair for matrix params. In the following URL, you need to call encodeUriSegment on "html",
|
||||
* "mk", and "mv":
|
||||
*
|
||||
* http://www.site.org/html;mk=mv?k=v#f
|
||||
*/
|
||||
export function encodeUriSegment(s: string): string {
|
||||
return encodeUriString(s)
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/%2B/gi, '+')
|
||||
.replace(/%26/gi, '&');
|
||||
}
|
||||
|
||||
export function decode(s: string): string {
|
||||
@ -350,18 +386,21 @@ export function decode(s: string): string {
|
||||
}
|
||||
|
||||
export function serializePath(path: UrlSegment): string {
|
||||
return `${encode(path.path)}${serializeParams(path.parameters)}`;
|
||||
return `${encodeUriSegment(path.path)}${serializeMatrixParams(path.parameters)}`;
|
||||
}
|
||||
|
||||
function serializeParams(params: {[key: string]: string}): string {
|
||||
return Object.keys(params).map(key => `;${encode(key)}=${encode(params[key])}`).join('');
|
||||
function serializeMatrixParams(params: {[key: string]: string}): string {
|
||||
return Object.keys(params)
|
||||
.map(key => `;${encodeUriSegment(key)}=${encodeUriSegment(params[key])}`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function serializeQueryParams(params: {[key: string]: any}): string {
|
||||
const strParams: string[] = Object.keys(params).map((name) => {
|
||||
const value = params[name];
|
||||
return Array.isArray(value) ? value.map(v => `${encode(name)}=${encode(v)}`).join('&') :
|
||||
`${encode(name)}=${encode(value)}`;
|
||||
return Array.isArray(value) ?
|
||||
value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') :
|
||||
`${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
|
||||
});
|
||||
|
||||
return strParams.length ? `?${strParams.join("&")}` : '';
|
||||
@ -414,7 +453,7 @@ class UrlParser {
|
||||
}
|
||||
|
||||
parseFragment(): string|null {
|
||||
return this.consumeOptional('#') ? decodeURI(this.remaining) : null;
|
||||
return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null;
|
||||
}
|
||||
|
||||
private parseChildren(): {[outlet: string]: UrlSegmentGroup} {
|
||||
|
Reference in New Issue
Block a user