Revert "fix(router): fix URL serialization so special characters are only encoded where needed (#22337)"
This reverts commit 094666da17
.
This commit is contained in:
parent
4180912538
commit
a4032296cc
|
@ -280,8 +280,7 @@ export class DefaultUrlSerializer implements UrlSerializer {
|
||||||
serialize(tree: UrlTree): string {
|
serialize(tree: UrlTree): string {
|
||||||
const segment = `/${serializeSegment(tree.root, true)}`;
|
const segment = `/${serializeSegment(tree.root, true)}`;
|
||||||
const query = serializeQueryParams(tree.queryParams);
|
const query = serializeQueryParams(tree.queryParams);
|
||||||
const fragment =
|
const fragment = typeof tree.fragment === `string` ? `#${encodeURI(tree.fragment !)}` : '';
|
||||||
typeof tree.fragment === `string` ? `#${encodeUriFragment(tree.fragment !)}` : '';
|
|
||||||
|
|
||||||
return `${segment}${query}${fragment}`;
|
return `${segment}${query}${fragment}`;
|
||||||
}
|
}
|
||||||
|
@ -327,10 +326,9 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes a URI string with the default encoding. This function will only ever be called from
|
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
|
||||||
* `encodeUriQuery` or `encodeUriSegment` as it's the base set of encodings to be used. We need
|
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
|
||||||
* a custom encoding because encodeURIComponent is too aggressive and encodes stuff that doesn't
|
* encoded per http://tools.ietf.org/html/rfc3986:
|
||||||
* have to be encoded per http://tools.ietf.org/html/rfc3986:
|
|
||||||
* query = *( pchar / "/" / "?" )
|
* query = *( pchar / "/" / "?" )
|
||||||
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||||||
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||||||
|
@ -338,47 +336,13 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||||
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||||||
* / "*" / "+" / "," / ";" / "="
|
* / "*" / "+" / "," / ";" / "="
|
||||||
*/
|
*/
|
||||||
function encodeUriString(s: string): string {
|
export function encode(s: string): string {
|
||||||
return encodeURIComponent(s)
|
return encodeURIComponent(s)
|
||||||
.replace(/%40/g, '@')
|
.replace(/%40/g, '@')
|
||||||
.replace(/%3A/gi, ':')
|
.replace(/%3A/gi, ':')
|
||||||
.replace(/%24/g, '$')
|
.replace(/%24/g, '$')
|
||||||
.replace(/%2C/gi, ',');
|
.replace(/%2C/gi, ',')
|
||||||
}
|
.replace(/%3B/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 {
|
export function decode(s: string): string {
|
||||||
|
@ -386,21 +350,18 @@ export function decode(s: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializePath(path: UrlSegment): string {
|
export function serializePath(path: UrlSegment): string {
|
||||||
return `${encodeUriSegment(path.path)}${serializeMatrixParams(path.parameters)}`;
|
return `${encode(path.path)}${serializeParams(path.parameters)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeMatrixParams(params: {[key: string]: string}): string {
|
function serializeParams(params: {[key: string]: string}): string {
|
||||||
return Object.keys(params)
|
return Object.keys(params).map(key => `;${encode(key)}=${encode(params[key])}`).join('');
|
||||||
.map(key => `;${encodeUriSegment(key)}=${encodeUriSegment(params[key])}`)
|
|
||||||
.join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeQueryParams(params: {[key: string]: any}): string {
|
function serializeQueryParams(params: {[key: string]: any}): string {
|
||||||
const strParams: string[] = Object.keys(params).map((name) => {
|
const strParams: string[] = Object.keys(params).map((name) => {
|
||||||
const value = params[name];
|
const value = params[name];
|
||||||
return Array.isArray(value) ?
|
return Array.isArray(value) ? value.map(v => `${encode(name)}=${encode(v)}`).join('&') :
|
||||||
value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') :
|
`${encode(name)}=${encode(value)}`;
|
||||||
`${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return strParams.length ? `?${strParams.join("&")}` : '';
|
return strParams.length ? `?${strParams.join("&")}` : '';
|
||||||
|
@ -453,7 +414,7 @@ class UrlParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseFragment(): string|null {
|
parseFragment(): string|null {
|
||||||
return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null;
|
return this.consumeOptional('#') ? decodeURI(this.remaining) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseChildren(): {[outlet: string]: UrlSegmentGroup} {
|
private parseChildren(): {[outlet: string]: UrlSegmentGroup} {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {PRIMARY_OUTLET} from '../src/shared';
|
import {PRIMARY_OUTLET} from '../src/shared';
|
||||||
import {DefaultUrlSerializer, UrlSegmentGroup, encodeUriQuery, encodeUriSegment, serializePath} from '../src/url_tree';
|
import {DefaultUrlSerializer, UrlSegmentGroup, encode, serializePath} from '../src/url_tree';
|
||||||
|
|
||||||
describe('url serializer', () => {
|
describe('url serializer', () => {
|
||||||
const url = new DefaultUrlSerializer();
|
const url = new DefaultUrlSerializer();
|
||||||
|
@ -189,7 +189,7 @@ describe('url serializer', () => {
|
||||||
describe('encoding/decoding', () => {
|
describe('encoding/decoding', () => {
|
||||||
it('should encode/decode path segments and parameters', () => {
|
it('should encode/decode path segments and parameters', () => {
|
||||||
const u =
|
const u =
|
||||||
`/${encodeUriSegment("one two")};${encodeUriSegment("p 1")}=${encodeUriSegment("v 1")};${encodeUriSegment("p 2")}=${encodeUriSegment("v 2")}`;
|
`/${encode("one two")};${encode("p 1")}=${encode("v 1")};${encode("p 2")}=${encode("v 2")}`;
|
||||||
const tree = url.parse(u);
|
const tree = url.parse(u);
|
||||||
|
|
||||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
|
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
|
||||||
|
@ -199,8 +199,7 @@ describe('url serializer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode/decode "slash" in path segments and parameters', () => {
|
it('should encode/decode "slash" in path segments and parameters', () => {
|
||||||
const u =
|
const u = `/${encode("one/two")};${encode("p/1")}=${encode("v/1")}/three`;
|
||||||
`/${encodeUriSegment("one/two")};${encodeUriSegment("p/1")}=${encodeUriSegment("v/1")}/three`;
|
|
||||||
const tree = url.parse(u);
|
const tree = url.parse(u);
|
||||||
const segment = tree.root.children[PRIMARY_OUTLET].segments[0];
|
const segment = tree.root.children[PRIMARY_OUTLET].segments[0];
|
||||||
expect(segment.path).toEqual('one/two');
|
expect(segment.path).toEqual('one/two');
|
||||||
|
@ -211,8 +210,7 @@ describe('url serializer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode/decode query params', () => {
|
it('should encode/decode query params', () => {
|
||||||
const u =
|
const u = `/one?${encode("p 1")}=${encode("v 1")}&${encode("p 2")}=${encode("v 2")}`;
|
||||||
`/one?${encodeUriQuery("p 1")}=${encodeUriQuery("v 1")}&${encodeUriQuery("p 2")}=${encodeUriQuery("v 2")}`;
|
|
||||||
const tree = url.parse(u);
|
const tree = url.parse(u);
|
||||||
|
|
||||||
expect(tree.queryParams).toEqual({'p 1': 'v 1', 'p 2': 'v 2'});
|
expect(tree.queryParams).toEqual({'p 1': 'v 1', 'p 2': 'v 2'});
|
||||||
|
@ -222,126 +220,27 @@ describe('url serializer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode query params leaving sub-delimiters intact', () => {
|
it('should encode query params leaving sub-delimiters intact', () => {
|
||||||
const percentChars = '#&+=[] ';
|
const percentChars = '/?#[]&+= ';
|
||||||
const percentCharsEncoded = '%23%26%2B%3D%5B%5D%20';
|
const percentCharsEncoded = '%2F%3F%23%5B%5D%26%2B%3D%20';
|
||||||
const intactChars = '/?!$\'()*,;:';
|
const intactChars = '!$\'()*,;:';
|
||||||
const params = percentChars + intactChars;
|
const params = percentChars + intactChars;
|
||||||
const paramsEncoded = percentCharsEncoded + intactChars;
|
const paramsEncoded = percentCharsEncoded + intactChars;
|
||||||
const mixedCaseString = 'sTrInG';
|
const mixedCaseString = 'sTrInG';
|
||||||
|
|
||||||
expect(percentCharsEncoded).toEqual(encodeUriQuery(percentChars));
|
expect(percentCharsEncoded).toEqual(encode(percentChars));
|
||||||
expect(intactChars).toEqual(encodeUriQuery(intactChars));
|
expect(intactChars).toEqual(encode(intactChars));
|
||||||
// Verify it replaces repeated characters correctly
|
// Verify it replaces repeated characters correctly
|
||||||
expect(paramsEncoded + paramsEncoded).toEqual(encodeUriQuery(params + params));
|
expect(paramsEncoded + paramsEncoded).toEqual(encode(params + params));
|
||||||
// Verify it doesn't change the case of alpha characters
|
// Verify it doesn't change the case of alpha characters
|
||||||
expect(mixedCaseString + paramsEncoded).toEqual(encodeUriQuery(mixedCaseString + params));
|
expect(mixedCaseString + paramsEncoded).toEqual(encode(mixedCaseString + params));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encode/decode fragment', () => {
|
it('should encode/decode fragment', () => {
|
||||||
const u = `/one#${encodeUriQuery('one two=three four')}`;
|
const u = `/one#${encodeURI("one two=three four")}`;
|
||||||
const tree = url.parse(u);
|
const tree = url.parse(u);
|
||||||
|
|
||||||
expect(tree.fragment).toEqual('one two=three four');
|
expect(tree.fragment).toEqual('one two=three four');
|
||||||
expect(url.serialize(tree)).toEqual('/one#one%20two%3Dthree%20four');
|
expect(url.serialize(tree)).toEqual(u);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('special character encoding/decoding', () => {
|
|
||||||
|
|
||||||
// Tests specific to https://github.com/angular/angular/issues/10280
|
|
||||||
it('should parse encoded parens in matrix params', () => {
|
|
||||||
const auxRoutesUrl = '/abc;foo=(other:val)';
|
|
||||||
const fooValueUrl = '/abc;foo=%28other:val%29';
|
|
||||||
|
|
||||||
const auxParsed = url.parse(auxRoutesUrl).root;
|
|
||||||
const fooParsed = url.parse(fooValueUrl).root;
|
|
||||||
|
|
||||||
|
|
||||||
// Test base case
|
|
||||||
expect(auxParsed.children[PRIMARY_OUTLET].segments.length).toBe(1);
|
|
||||||
expect(auxParsed.children[PRIMARY_OUTLET].segments[0].path).toBe('abc');
|
|
||||||
expect(auxParsed.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({foo: ''});
|
|
||||||
expect(auxParsed.children['other'].segments.length).toBe(1);
|
|
||||||
expect(auxParsed.children['other'].segments[0].path).toBe('val');
|
|
||||||
|
|
||||||
// Confirm matrix params are URL decoded
|
|
||||||
expect(fooParsed.children[PRIMARY_OUTLET].segments.length).toBe(1);
|
|
||||||
expect(fooParsed.children[PRIMARY_OUTLET].segments[0].path).toBe('abc');
|
|
||||||
expect(fooParsed.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({
|
|
||||||
foo: '(other:val)'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should serialize encoded parens in matrix params', () => {
|
|
||||||
const testUrl = '/abc;foo=%28one%29';
|
|
||||||
|
|
||||||
const parsed = url.parse(testUrl);
|
|
||||||
|
|
||||||
expect(url.serialize(parsed)).toBe('/abc;foo=%28one%29');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not serialize encoded parens in query params', () => {
|
|
||||||
const testUrl = '/abc?foo=%28one%29';
|
|
||||||
|
|
||||||
const parsed = url.parse(testUrl);
|
|
||||||
|
|
||||||
expect(parsed.queryParams).toEqual({foo: '(one)'});
|
|
||||||
|
|
||||||
expect(url.serialize(parsed)).toBe('/abc?foo=(one)');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test special characters in general
|
|
||||||
|
|
||||||
// From http://www.ietf.org/rfc/rfc3986.txt
|
|
||||||
const unreserved = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~`;
|
|
||||||
|
|
||||||
it('should encode a minimal set of special characters in queryParams', () => {
|
|
||||||
const notEncoded = unreserved + `:@!$'*,()/?;`;
|
|
||||||
const encode = ` +%&=#[]`;
|
|
||||||
const encoded = `%20%2B%25%26%3D%23%5B%5D`;
|
|
||||||
|
|
||||||
const parsed = url.parse('/foo');
|
|
||||||
|
|
||||||
parsed.queryParams = {notEncoded, encode};
|
|
||||||
|
|
||||||
expect(url.serialize(parsed)).toBe(`/foo?notEncoded=${notEncoded}&encode=${encoded}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encode a minimal set of special characters in fragment', () => {
|
|
||||||
const notEncoded = unreserved + `:@!$'*,()/?;#`;
|
|
||||||
const encode = ` +%&=[]`;
|
|
||||||
const encoded = `%20%2B%25%26%3D%5B%5D`;
|
|
||||||
|
|
||||||
const parsed = url.parse('/foo');
|
|
||||||
|
|
||||||
parsed.fragment = notEncoded + encode;
|
|
||||||
|
|
||||||
expect(url.serialize(parsed)).toBe(`/foo#${notEncoded}${encoded}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encode minimal special characters plus parens and semi-colon in matrix params',
|
|
||||||
() => {
|
|
||||||
const notEncoded = unreserved + `:@!$'*,+&`;
|
|
||||||
const encode = ` /%=#()[];?`;
|
|
||||||
const encoded = `%20%2F%25%3D%23%28%29%5B%5D%3B%3F`;
|
|
||||||
|
|
||||||
const parsed = url.parse('/foo');
|
|
||||||
|
|
||||||
parsed.root.children[PRIMARY_OUTLET].segments[0].parameters = {notEncoded, encode};
|
|
||||||
|
|
||||||
expect(url.serialize(parsed)).toBe(`/foo;notEncoded=${notEncoded};encode=${encoded}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should encode special characters in the path the same as matrix params', () => {
|
|
||||||
const notEncoded = unreserved + `:@!$'*,+&`;
|
|
||||||
const encode = ` /%=#()[];?`;
|
|
||||||
const encoded = `%20%2F%25%3D%23%28%29%5B%5D%3B%3F`;
|
|
||||||
|
|
||||||
const parsed = url.parse('/foo');
|
|
||||||
|
|
||||||
parsed.root.children[PRIMARY_OUTLET].segments[0].path = notEncoded + encode;
|
|
||||||
|
|
||||||
expect(url.serialize(parsed)).toBe(`/${notEncoded}${encoded}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue