Skip to content

Commit affd29d

Browse files
cherry-pick 02e9866 from 0.2.17
1 parent 7792134 commit affd29d

File tree

4 files changed

+129
-9
lines changed

4 files changed

+129
-9
lines changed

src/params/paramTypes.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import {is, val} from "../common/hof";
55
import {services} from "../common/coreservices";
66
import {Type} from "./type";
77

8-
const swapString = (search, replace) => val => val != null ? val.toString().replace(search, replace) : val;
9-
const valToString = swapString(/\//g, "%2F");
10-
const valFromString = swapString(/%2F/g, "/");
8+
// Use tildes to pre-encode slashes.
9+
// If the slashes are simply URLEncoded, the browser can choose to pre-decode them,
10+
// and bidirectional encoding/decoding fails.
11+
// Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character
12+
function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; }
13+
function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; }
1114

1215
class ParamTypes {
1316
types: any;

src/url/urlMatcher.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import {prop, propEq } from "../common/hof";
77
import {isArray, isString} from "../common/predicates";
88
import {Param, paramTypes} from "../params/module";
9+
import {isDefined} from "../common/predicates";
910

1011
interface params {
1112
$$validates: (params: string) => Array<string>;
@@ -283,10 +284,16 @@ export class UrlMatcher {
283284
if (param.replace[j].from === value) value = param.replace[j].to;
284285
}
285286
if (value && param.array === true) value = decodePathArray(value);
287+
if (isDefined(value)) value = param.type.decode(value);
286288
values[param.id] = param.value(value);
287289
}
288290
forEach(searchParams, param => {
289-
values[param.id] = param.value(search[param.id])
291+
let value = search[param.id];
292+
for (let j = 0; j < param.replace.length; j++) {
293+
if (param.replace[j].from === value) value = param.replace[j].to;
294+
}
295+
if (isDefined(value)) value = param.type.decode(value);
296+
values[param.id] = param.value(value);
290297
});
291298

292299
if (hash) values["#"] = hash;

test/stateSpec.js

+79-1
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ describe('state', function () {
329329
onEnter: function(badness) {}
330330
})
331331
.state('resolveTimeout', {
332-
url: "/:foo",
332+
url: "/resolve-timeout/:foo",
333333
resolve: {
334334
value: function ($timeout) {
335335
return $timeout(function() { log += "Success!"; }, 1);
@@ -1266,6 +1266,84 @@ describe('state', function () {
12661266
expect($state.current.name).toBe('');
12671267
}));
12681268

1269+
1270+
// Tests for issue #2339
1271+
describe("slashes in parameter values", function() {
1272+
1273+
var $rootScope, $state, $compile;
1274+
beforeEach(function () {
1275+
1276+
stateProvider.state('myState', {
1277+
template: 'myState',
1278+
url: '/my-state?:previous',
1279+
controller: function () {
1280+
log += 'myController;';
1281+
}
1282+
});
1283+
1284+
inject(function (_$rootScope_, _$state_, _$compile_, $trace) {
1285+
//$trace.enable(1);
1286+
$rootScope = _$rootScope_;
1287+
$state = _$state_;
1288+
$compile = _$compile_;
1289+
});
1290+
spyOn($state, 'go').and.callThrough();
1291+
spyOn($state, 'transitionTo').and.callThrough();
1292+
$compile('<div><div ui-view/></div>')($rootScope);
1293+
log = '';
1294+
});
1295+
1296+
describe('with no "/" in the params', function () {
1297+
beforeEach(function () {
1298+
$state.go('myState',{previous: 'last'});
1299+
$rootScope.$digest();
1300+
});
1301+
it('should call $state.go once', function() {
1302+
expect($state.go.calls.count()).toBe(1);
1303+
});
1304+
it('should call $state.transitionTo once', function() {
1305+
expect($state.transitionTo.calls.count()).toBe(1);
1306+
});
1307+
it('should call myController once', function() {
1308+
expect(log).toBe('myController;');
1309+
});
1310+
});
1311+
1312+
describe('with a "/" in the params', function () {
1313+
beforeEach(function () {
1314+
$state.go('myState',{previous: '/last'});
1315+
$rootScope.$digest();
1316+
});
1317+
it('should call $state.go once', function() {
1318+
expect($state.go.calls.count()).toBe(1);
1319+
});
1320+
it('should call $state.transitionTo once', function() {
1321+
expect($state.transitionTo.calls.count()).toBe(1);
1322+
});
1323+
it('should call myController once', function() {
1324+
expect(log).toBe('myController;');
1325+
});
1326+
});
1327+
1328+
describe('with an encoded "/" in the params', function () {
1329+
beforeEach(function () {
1330+
$state.go('myState',{previous: encodeURIComponent('/last')});
1331+
$rootScope.$digest();
1332+
});
1333+
it('should call $state.go once', function() {
1334+
expect($state.go.calls.count()).toBe(1);
1335+
});
1336+
it('should call $state.transitionTo once', function() {
1337+
expect($state.transitionTo.calls.count()).toBe(1);
1338+
});
1339+
it('should call myController once', function() {
1340+
expect(log).toBe('myController;');
1341+
});
1342+
});
1343+
});
1344+
1345+
1346+
12691347
describe("typed parameter handling", function() {
12701348
beforeEach(function () {
12711349
stateProvider.state({

test/urlMatcherFactorySpec.js

+36-4
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,40 @@ describe("UrlMatcher", function () {
7878
expect(matcher.format(array)).toBe('/?foo=bar&foo=baz');
7979
});
8080

81-
it("should encode and decode slashes in parameter values", function () {
82-
var matcher = new UrlMatcher('/:foo');
83-
expect(matcher.format({ foo: "/" })).toBe('/%252F');
84-
expect(matcher.format({ foo: "//" })).toBe('/%252F%252F');
81+
it("should encode and decode slashes in parameter values as ~2F", function () {
82+
var matcher1 = new UrlMatcher('/:foo');
83+
84+
expect(matcher1.format({ foo: "/" })).toBe('/~2F');
85+
expect(matcher1.format({ foo: "//" })).toBe('/~2F~2F');
86+
87+
expect(matcher1.exec('/')).toBeTruthy();
88+
expect(matcher1.exec('//')).not.toBeTruthy();
89+
90+
expect(matcher1.exec('/').foo).toBe("");
91+
expect(matcher1.exec('/123').foo).toBe("123");
92+
expect(matcher1.exec('/~2F').foo).toBe("/");
93+
expect(matcher1.exec('/123~2F').foo).toBe("123/");
94+
95+
// param :foo should match between two slashes
96+
var matcher2 = new UrlMatcher('/:foo/');
97+
98+
expect(matcher2.exec('/')).not.toBeTruthy();
99+
expect(matcher2.exec('//')).toBeTruthy();
100+
101+
expect(matcher2.exec('//').foo).toBe("");
102+
expect(matcher2.exec('/123/').foo).toBe("123");
103+
expect(matcher2.exec('/~2F/').foo).toBe("/");
104+
expect(matcher2.exec('/123~2F/').foo).toBe("123/");
105+
});
106+
107+
it("should encode and decode tildes in parameter values as ~~", function () {
108+
var matcher1 = new UrlMatcher('/:foo');
109+
110+
expect(matcher1.format({ foo: "abc" })).toBe('/abc');
111+
expect(matcher1.format({ foo: "~abc" })).toBe('/~~abc');
112+
113+
expect(matcher1.exec('/abc').foo).toBe("abc");
114+
expect(matcher1.exec('/~~abc').foo).toBe("~abc");
85115
});
86116

87117
describe("snake-case parameters", function() {
@@ -344,6 +374,8 @@ describe("UrlMatcher", function () {
344374

345375
$location.url("/foo");
346376
expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } );
377+
$location.url("/foo?param1=");
378+
expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } );
347379
$location.url("/foo?param1=bar");
348380
expect(m.exec($location.path(), $location.search())).toEqual( { param1: [ 'bar' ] } );
349381
$location.url("/foo?param1=bar&param1=baz");

0 commit comments

Comments
 (0)