diff --git a/packages/router/src/url_tree.ts b/packages/router/src/url_tree.ts index 6e42b5d1c5..63c3a7ea01 100644 --- a/packages/router/src/url_tree.ts +++ b/packages/router/src/url_tree.ts @@ -280,8 +280,7 @@ 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` ? `#${encodeUriFragment(tree.fragment !)}` : ''; + const fragment = typeof tree.fragment === `string` ? `#${encodeURI(tree.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 - * `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: + * 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: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" @@ -338,47 +336,13 @@ function serializeSegment(segment: UrlSegmentGroup, root: boolean): string { * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ -function encodeUriString(s: string): string { +export function encode(s: string): string { return encodeURIComponent(s) .replace(/%40/g, '@') .replace(/%3A/gi, ':') .replace(/%24/g, '$') - .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, '&'); + .replace(/%2C/gi, ',') + .replace(/%3B/gi, ';'); } export function decode(s: string): string { @@ -386,21 +350,18 @@ export function decode(s: string): 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 { - return Object.keys(params) - .map(key => `;${encodeUriSegment(key)}=${encodeUriSegment(params[key])}`) - .join(''); +function serializeParams(params: {[key: string]: string}): string { + return Object.keys(params).map(key => `;${encode(key)}=${encode(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 => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') : - `${encodeUriQuery(name)}=${encodeUriQuery(value)}`; + return Array.isArray(value) ? value.map(v => `${encode(name)}=${encode(v)}`).join('&') : + `${encode(name)}=${encode(value)}`; }); return strParams.length ? `?${strParams.join("&")}` : ''; @@ -453,7 +414,7 @@ class UrlParser { } parseFragment(): string|null { - return this.consumeOptional('#') ? decodeURIComponent(this.remaining) : null; + return this.consumeOptional('#') ? decodeURI(this.remaining) : null; } private parseChildren(): {[outlet: string]: UrlSegmentGroup} { diff --git a/packages/router/test/url_serializer.spec.ts b/packages/router/test/url_serializer.spec.ts index 29d78cf673..a318011b10 100644 --- a/packages/router/test/url_serializer.spec.ts +++ b/packages/router/test/url_serializer.spec.ts @@ -7,7 +7,7 @@ */ 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', () => { const url = new DefaultUrlSerializer(); @@ -189,7 +189,7 @@ describe('url serializer', () => { describe('encoding/decoding', () => { it('should encode/decode path segments and parameters', () => { 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); 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', () => { - const u = - `/${encodeUriSegment("one/two")};${encodeUriSegment("p/1")}=${encodeUriSegment("v/1")}/three`; + const u = `/${encode("one/two")};${encode("p/1")}=${encode("v/1")}/three`; const tree = url.parse(u); const segment = tree.root.children[PRIMARY_OUTLET].segments[0]; expect(segment.path).toEqual('one/two'); @@ -211,8 +210,7 @@ describe('url serializer', () => { }); it('should encode/decode query params', () => { - const u = - `/one?${encodeUriQuery("p 1")}=${encodeUriQuery("v 1")}&${encodeUriQuery("p 2")}=${encodeUriQuery("v 2")}`; + const u = `/one?${encode("p 1")}=${encode("v 1")}&${encode("p 2")}=${encode("v 2")}`; const tree = url.parse(u); 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', () => { - const percentChars = '#&+=[] '; - const percentCharsEncoded = '%23%26%2B%3D%5B%5D%20'; - const intactChars = '/?!$\'()*,;:'; + const percentChars = '/?#[]&+= '; + const percentCharsEncoded = '%2F%3F%23%5B%5D%26%2B%3D%20'; + const intactChars = '!$\'()*,;:'; const params = percentChars + intactChars; const paramsEncoded = percentCharsEncoded + intactChars; const mixedCaseString = 'sTrInG'; - expect(percentCharsEncoded).toEqual(encodeUriQuery(percentChars)); - expect(intactChars).toEqual(encodeUriQuery(intactChars)); + expect(percentCharsEncoded).toEqual(encode(percentChars)); + expect(intactChars).toEqual(encode(intactChars)); // 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 - expect(mixedCaseString + paramsEncoded).toEqual(encodeUriQuery(mixedCaseString + params)); + expect(mixedCaseString + paramsEncoded).toEqual(encode(mixedCaseString + params)); }); 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); expect(tree.fragment).toEqual('one two=three four'); - expect(url.serialize(tree)).toEqual('/one#one%20two%3Dthree%20four'); - }); - }); - - 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}`); + expect(url.serialize(tree)).toEqual(u); }); });