Skip to content

Commit 02e9866

Browse files
fix(UrlMatcher): Properly encode/decode slashes in parameters
Closes #2339 Closes #2172 Closes #2250 Closes #1119
1 parent 20d6e24 commit 02e9866

File tree

4 files changed

+130
-8
lines changed

4 files changed

+130
-8
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"karma-coffee-preprocessor": "~0.1.0",
5959
"karma": "~0.10.4",
6060
"karma-phantomjs-launcher": "~0.1.0",
61+
"requirejs": "^2.1.22",
6162
"load-grunt-tasks": "~0.4.0",
6263
"grunt-conventional-changelog": "~1.1.0",
6364
"grunt-ngdocs": "~0.2.5"

src/urlMatcherFactory.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -256,20 +256,29 @@ UrlMatcher.prototype.exec = function (path, searchParams) {
256256
return map(allReversed, unquoteDashes).reverse();
257257
}
258258

259+
var param, paramVal;
259260
for (i = 0; i < nPath; i++) {
260261
paramName = paramNames[i];
261-
var param = this.params[paramName];
262-
var paramVal = m[i+1];
262+
param = this.params[paramName];
263+
paramVal = m[i+1];
263264
// if the param value matches a pre-replace pair, replace the value before decoding.
264265
for (j = 0; j < param.replace.length; j++) {
265266
if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
266267
}
267268
if (paramVal && param.array === true) paramVal = decodePathArray(paramVal);
269+
if (isDefined(paramVal)) paramVal = param.type.decode(paramVal);
268270
values[paramName] = param.value(paramVal);
269271
}
270272
for (/**/; i < nTotal; i++) {
271273
paramName = paramNames[i];
272274
values[paramName] = this.params[paramName].value(searchParams[paramName]);
275+
param = this.params[paramName];
276+
paramVal = searchParams[paramName];
277+
for (j = 0; j < param.replace.length; j++) {
278+
if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
279+
}
280+
if (isDefined(paramVal)) paramVal = param.type.decode(paramVal);
281+
values[paramName] = param.value(paramVal);
273282
}
274283

275284
return values;
@@ -582,8 +591,12 @@ function $UrlMatcherFactory() {
582591

583592
var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false;
584593

585-
function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; }
586-
function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; }
594+
// Use tildes to pre-encode slashes.
595+
// If the slashes are simply URLEncoded, the browser can choose to pre-decode them,
596+
// and bidirectional encoding/decoding fails.
597+
// Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character
598+
function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; }
599+
function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; }
587600

588601
var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = {
589602
string: {

test/stateSpec.js

+76
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,82 @@ describe('state', function () {
11691169
expect($state.current.name).toBe('');
11701170
}));
11711171

1172+
1173+
// Tests for issue #2339
1174+
describe("slashes in parameter values", function() {
1175+
1176+
var $rootScope, $state, $compile;
1177+
beforeEach(function () {
1178+
1179+
stateProvider.state('myState', {
1180+
url: '/my-state?:previous',
1181+
controller: function () {
1182+
log += 'myController;';
1183+
}
1184+
});
1185+
1186+
inject(function (_$rootScope_, _$state_, _$compile_) {
1187+
$rootScope = _$rootScope_;
1188+
$state = _$state_;
1189+
$compile = _$compile_;
1190+
});
1191+
spyOn($state, 'go').andCallThrough();
1192+
spyOn($state, 'transitionTo').andCallThrough();
1193+
$compile('<div><div ui-view/></div>')($rootScope);
1194+
log = '';
1195+
});
1196+
1197+
describe('with no "/" in the params', function () {
1198+
beforeEach(function () {
1199+
$state.go('myState',{previous: 'last'});
1200+
$rootScope.$digest();
1201+
});
1202+
it('should call $state.go once', function() {
1203+
expect($state.go.calls.length).toBe(1);
1204+
});
1205+
it('should call $state.transitionTo once', function() {
1206+
expect($state.transitionTo.calls.length).toBe(1);
1207+
});
1208+
it('should call myController once', function() {
1209+
expect(log).toBe('myController;');
1210+
});
1211+
});
1212+
1213+
describe('with a "/" in the params', function () {
1214+
beforeEach(function () {
1215+
$state.go('myState',{previous: '/last'});
1216+
$rootScope.$digest();
1217+
});
1218+
it('should call $state.go once', function() {
1219+
expect($state.go.calls.length).toBe(1);
1220+
});
1221+
it('should call $state.transitionTo once', function() {
1222+
expect($state.transitionTo.calls.length).toBe(1);
1223+
});
1224+
it('should call myController once', function() {
1225+
expect(log).toBe('myController;');
1226+
});
1227+
});
1228+
1229+
describe('with an encoded "/" in the params', function () {
1230+
beforeEach(function () {
1231+
$state.go('myState',{previous: encodeURIComponent('/last')});
1232+
$rootScope.$digest();
1233+
});
1234+
it('should call $state.go once', function() {
1235+
expect($state.go.calls.length).toBe(1);
1236+
});
1237+
it('should call $state.transitionTo once', function() {
1238+
expect($state.transitionTo.calls.length).toBe(1);
1239+
});
1240+
it('should call myController once', function() {
1241+
expect(log).toBe('myController;');
1242+
});
1243+
});
1244+
});
1245+
1246+
1247+
11721248
describe("typed parameter handling", function() {
11731249
beforeEach(function () {
11741250
stateProvider.state({

test/urlMatcherFactorySpec.js

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

64-
it("should encode and decode slashes in parameter values", function () {
65-
var matcher = new UrlMatcher('/:foo');
66-
expect(matcher.format({ foo: "/" })).toBe('/%252F');
67-
expect(matcher.format({ foo: "//" })).toBe('/%252F%252F');
64+
it("should encode and decode slashes in parameter values as ~2F", function () {
65+
var matcher1 = new UrlMatcher('/:foo');
66+
67+
expect(matcher1.format({ foo: "/" })).toBe('/~2F');
68+
expect(matcher1.format({ foo: "//" })).toBe('/~2F~2F');
69+
70+
expect(matcher1.exec('/')).toBeTruthy();
71+
expect(matcher1.exec('//')).not.toBeTruthy();
72+
73+
expect(matcher1.exec('/').foo).toBe("");
74+
expect(matcher1.exec('/123').foo).toBe("123");
75+
expect(matcher1.exec('/~2F').foo).toBe("/");
76+
expect(matcher1.exec('/123~2F').foo).toBe("123/");
77+
78+
// param :foo should match between two slashes
79+
var matcher2 = new UrlMatcher('/:foo/');
80+
81+
expect(matcher2.exec('/')).not.toBeTruthy();
82+
expect(matcher2.exec('//')).toBeTruthy();
83+
84+
expect(matcher2.exec('//').foo).toBe("");
85+
expect(matcher2.exec('/123/').foo).toBe("123");
86+
expect(matcher2.exec('/~2F/').foo).toBe("/");
87+
expect(matcher2.exec('/123~2F/').foo).toBe("123/");
88+
});
89+
90+
it("should encode and decode tildes in parameter values as ~~", function () {
91+
var matcher1 = new UrlMatcher('/:foo');
92+
93+
expect(matcher1.format({ foo: "abc" })).toBe('/abc');
94+
expect(matcher1.format({ foo: "~abc" })).toBe('/~~abc');
95+
96+
expect(matcher1.exec('/abc').foo).toBe("abc");
97+
expect(matcher1.exec('/~~abc').foo).toBe("~abc");
6898
});
6999

70100
describe("snake-case parameters", function() {
@@ -306,6 +336,8 @@ describe("UrlMatcher", function () {
306336

307337
$location.url("/foo");
308338
expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } );
339+
$location.url("/foo?param1=");
340+
expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } );
309341
$location.url("/foo?param1=bar");
310342
expect(m.exec($location.path(), $location.search())).toEqual( { param1: [ 'bar' ] } );
311343
$location.url("/foo?param1=bar&param1=baz");

0 commit comments

Comments
 (0)