Skip to content

Commit

Permalink
fixup! fix(router): fix URL serialization so special characters are o…
Browse files Browse the repository at this point in the history
…nly encoded where needed
  • Loading branch information
jasonaden committed Feb 21, 2018
1 parent b5d4bdf commit 0e008dd
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 25 deletions.
40 changes: 26 additions & 14 deletions packages/router/src/url_tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` ? `#${encodeUriParams(tree.fragment !)}` : '';
const fragment = typeof tree.fragment === `string` ? `#${encodeUriQuery(tree.fragment !)}` : '';

return `${segment}${query}${fragment}`;
}
Expand Down Expand Up @@ -327,17 +326,18 @@ 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 / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
export function encodeUriParams(s: string, type: 'query' | 'segment' = 'query'): string {
function encodeUriString(s: string, type: 'query' | 'segment' = 'query'): string {
let encoded = encodeURIComponent(s)
.replace(/%40/g, '@')
.replace(/%3A/gi, ':')
Expand All @@ -346,33 +346,45 @@ export function encodeUriParams(s: string, type: 'query' | 'segment' = 'query'):
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']');

encoded = type === 'segment' ?
encoded.replace(/\(/g, '%28').replace(/\)/g, '%29') :
encoded.replace(/%3B/gi, ';').replace(/%3F/gi, '?').replace(/%2F/gi, '/');

return encoded;
}

/**
* Encodes a string for use in the query string or fragment. This function should be used to
* encode both keys and values in a query string key/value pair as well as the fragment string.
*/
export function encodeUriQuery(s: string): string {
return encodeUriString(s).replace(/%3B/gi, ';').replace(/%3F/gi, '?').replace(/%2F/gi, '/');
}

/**
* Encodes for the URI segment. This is the path portion of a URI, before the query string
* and/or fragment.
*/
export function encodeUriSegment(s: string): string {
return encodeUriString(s).replace(/\(/g, '%28').replace(/\)/g, '%29');
}

export function decode(s: string): string {
return decodeURIComponent(s);
}

export function serializePath(path: UrlSegment): string {
return `${encodeUriParams(path.path, 'segment')}${serializeParams(path.parameters)}`;
return `${encodeUriSegment(path.path)}${serializeParams(path.parameters)}`;
}

function serializeParams(params: {[key: string]: string}): string {
return Object.keys(params)
.map(key => `;${encodeUriParams(key, 'segment')}=${encodeUriParams(params[key], 'segment')}`)
.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 => `${encodeUriParams(name)}=${encodeUriParams(v)}`).join('&') :
`${encodeUriParams(name)}=${encodeUriParams(value)}`;
value.map(v => `${encodeUriQuery(name)}=${encodeUriQuery(v)}`).join('&') :
`${encodeUriQuery(name)}=${encodeUriQuery(value)}`;
});

return strParams.length ? `?${strParams.join("&")}` : '';
Expand Down
20 changes: 9 additions & 11 deletions packages/router/test/url_serializer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegmentGroup, encodeUriParams, serializePath} from '../src/url_tree';
import {DefaultUrlSerializer, UrlSegmentGroup, encodeUriQuery, encodeUriSegment, serializePath} from '../src/url_tree';

describe('url serializer', () => {
const url = new DefaultUrlSerializer();
Expand Down Expand Up @@ -189,7 +189,7 @@ describe('url serializer', () => {
describe('encoding/decoding', () => {
it('should encode/decode path segments and parameters', () => {
const u =
`/${encodeUriParams("one two")};${encodeUriParams("p 1")}=${encodeUriParams("v 1")};${encodeUriParams("p 2")}=${encodeUriParams("v 2")}`;
`/${encodeUriQuery("one two")};${encodeUriQuery("p 1")}=${encodeUriQuery("v 1")};${encodeUriQuery("p 2")}=${encodeUriQuery("v 2")}`;
const tree = url.parse(u);

expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
Expand All @@ -200,7 +200,7 @@ describe('url serializer', () => {

it('should encode/decode "slash" in path segments and parameters', () => {
const u =
`/${encodeUriParams("one/two", 'segment')};${encodeUriParams("p/1", 'segment')}=${encodeUriParams("v/1", 'segment')}/three`;
`/${encodeUriSegment("one/two")};${encodeUriSegment("p/1")}=${encodeUriSegment("v/1")}/three`;
const tree = url.parse(u);
const segment = tree.root.children[PRIMARY_OUTLET].segments[0];
expect(segment.path).toEqual('one/two');
Expand All @@ -212,7 +212,7 @@ describe('url serializer', () => {

it('should encode/decode query params', () => {
const u =
`/one?${encodeUriParams("p 1")}=${encodeUriParams("v 1")}&${encodeUriParams("p 2")}=${encodeUriParams("v 2")}`;
`/one?${encodeUriQuery("p 1")}=${encodeUriQuery("v 1")}&${encodeUriQuery("p 2")}=${encodeUriQuery("v 2")}`;
const tree = url.parse(u);

expect(tree.queryParams).toEqual({'p 1': 'v 1', 'p 2': 'v 2'});
Expand All @@ -229,16 +229,16 @@ describe('url serializer', () => {
const paramsEncoded = percentCharsEncoded + intactChars;
const mixedCaseString = 'sTrInG';

expect(percentCharsEncoded).toEqual(encodeUriParams(percentChars));
expect(intactChars).toEqual(encodeUriParams(intactChars));
expect(percentCharsEncoded).toEqual(encodeUriQuery(percentChars));
expect(intactChars).toEqual(encodeUriQuery(intactChars));
// Verify it replaces repeated characters correctly
expect(paramsEncoded + paramsEncoded).toEqual(encodeUriParams(params + params));
expect(paramsEncoded + paramsEncoded).toEqual(encodeUriQuery(params + params));
// Verify it doesn't change the case of alpha characters
expect(mixedCaseString + paramsEncoded).toEqual(encodeUriParams(mixedCaseString + params));
expect(mixedCaseString + paramsEncoded).toEqual(encodeUriQuery(mixedCaseString + params));
});

it('should encode/decode fragment', () => {
const u = `/one#${encodeUriParams('one two=three four')}`;
const u = `/one#${encodeUriQuery('one two=three four')}`;
const tree = url.parse(u);

expect(tree.fragment).toEqual('one two=three four');
Expand All @@ -247,8 +247,6 @@ describe('url serializer', () => {
});

describe('special character encoding/decoding', () => {
const pathSpecialCharacters = `"'()[]*#@`
const paramSpecialCharacters = `"'()[]*#@`

// Tests specific to https://github.com/angular/angular/issues/10280
it('should parse encoded parens in matrix params', () => {
Expand Down

0 comments on commit 0e008dd

Please sign in to comment.