diff --git a/package.json b/package.json index 7076faef7..7c339790d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "karma-coffee-preprocessor": "~0.1.0", "karma": "~0.10.4", "karma-phantomjs-launcher": "~0.1.0", + "requirejs": "^2.1.22", "load-grunt-tasks": "~0.4.0", "grunt-conventional-changelog": "~1.1.0", "grunt-ngdocs": "~0.2.5" diff --git a/src/urlMatcherFactory.js b/src/urlMatcherFactory.js index 1b0422d07..30ff0be0b 100644 --- a/src/urlMatcherFactory.js +++ b/src/urlMatcherFactory.js @@ -256,20 +256,29 @@ UrlMatcher.prototype.exec = function (path, searchParams) { return map(allReversed, unquoteDashes).reverse(); } + var param, paramVal; for (i = 0; i < nPath; i++) { paramName = paramNames[i]; - var param = this.params[paramName]; - var paramVal = m[i+1]; + param = this.params[paramName]; + paramVal = m[i+1]; // if the param value matches a pre-replace pair, replace the value before decoding. for (j = 0; j < param.replace.length; j++) { if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; } if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); + if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); values[paramName] = param.value(paramVal); } for (/**/; i < nTotal; i++) { paramName = paramNames[i]; values[paramName] = this.params[paramName].value(searchParams[paramName]); + param = this.params[paramName]; + paramVal = searchParams[paramName]; + for (j = 0; j < param.replace.length; j++) { + if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; + } + if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); + values[paramName] = param.value(paramVal); } return values; @@ -582,8 +591,12 @@ function $UrlMatcherFactory() { var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; - function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; } - function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; } + // Use tildes to pre-encode slashes. + // If the slashes are simply URLEncoded, the browser can choose to pre-decode them, + // and bidirectional encoding/decoding fails. + // Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character + function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; } + function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; } var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { string: { diff --git a/test/stateSpec.js b/test/stateSpec.js index c73a7fa0c..9c3d7da6b 100644 --- a/test/stateSpec.js +++ b/test/stateSpec.js @@ -1169,6 +1169,82 @@ describe('state', function () { expect($state.current.name).toBe(''); })); + + // Tests for issue #2339 + describe("slashes in parameter values", function() { + + var $rootScope, $state, $compile; + beforeEach(function () { + + stateProvider.state('myState', { + url: '/my-state?:previous', + controller: function () { + log += 'myController;'; + } + }); + + inject(function (_$rootScope_, _$state_, _$compile_) { + $rootScope = _$rootScope_; + $state = _$state_; + $compile = _$compile_; + }); + spyOn($state, 'go').andCallThrough(); + spyOn($state, 'transitionTo').andCallThrough(); + $compile('
')($rootScope); + log = ''; + }); + + describe('with no "/" in the params', function () { + beforeEach(function () { + $state.go('myState',{previous: 'last'}); + $rootScope.$digest(); + }); + it('should call $state.go once', function() { + expect($state.go.calls.length).toBe(1); + }); + it('should call $state.transitionTo once', function() { + expect($state.transitionTo.calls.length).toBe(1); + }); + it('should call myController once', function() { + expect(log).toBe('myController;'); + }); + }); + + describe('with a "/" in the params', function () { + beforeEach(function () { + $state.go('myState',{previous: '/last'}); + $rootScope.$digest(); + }); + it('should call $state.go once', function() { + expect($state.go.calls.length).toBe(1); + }); + it('should call $state.transitionTo once', function() { + expect($state.transitionTo.calls.length).toBe(1); + }); + it('should call myController once', function() { + expect(log).toBe('myController;'); + }); + }); + + describe('with an encoded "/" in the params', function () { + beforeEach(function () { + $state.go('myState',{previous: encodeURIComponent('/last')}); + $rootScope.$digest(); + }); + it('should call $state.go once', function() { + expect($state.go.calls.length).toBe(1); + }); + it('should call $state.transitionTo once', function() { + expect($state.transitionTo.calls.length).toBe(1); + }); + it('should call myController once', function() { + expect(log).toBe('myController;'); + }); + }); + }); + + + describe("typed parameter handling", function() { beforeEach(function () { stateProvider.state({ diff --git a/test/urlMatcherFactorySpec.js b/test/urlMatcherFactorySpec.js index 3ef72a8f6..e3c20d695 100755 --- a/test/urlMatcherFactorySpec.js +++ b/test/urlMatcherFactorySpec.js @@ -61,10 +61,40 @@ describe("UrlMatcher", function () { expect(matcher.format(array)).toBe('/?foo=bar&foo=baz'); }); - it("should encode and decode slashes in parameter values", function () { - var matcher = new UrlMatcher('/:foo'); - expect(matcher.format({ foo: "/" })).toBe('/%252F'); - expect(matcher.format({ foo: "//" })).toBe('/%252F%252F'); + it("should encode and decode slashes in parameter values as ~2F", function () { + var matcher1 = new UrlMatcher('/:foo'); + + expect(matcher1.format({ foo: "/" })).toBe('/~2F'); + expect(matcher1.format({ foo: "//" })).toBe('/~2F~2F'); + + expect(matcher1.exec('/')).toBeTruthy(); + expect(matcher1.exec('//')).not.toBeTruthy(); + + expect(matcher1.exec('/').foo).toBe(""); + expect(matcher1.exec('/123').foo).toBe("123"); + expect(matcher1.exec('/~2F').foo).toBe("/"); + expect(matcher1.exec('/123~2F').foo).toBe("123/"); + + // param :foo should match between two slashes + var matcher2 = new UrlMatcher('/:foo/'); + + expect(matcher2.exec('/')).not.toBeTruthy(); + expect(matcher2.exec('//')).toBeTruthy(); + + expect(matcher2.exec('//').foo).toBe(""); + expect(matcher2.exec('/123/').foo).toBe("123"); + expect(matcher2.exec('/~2F/').foo).toBe("/"); + expect(matcher2.exec('/123~2F/').foo).toBe("123/"); + }); + + it("should encode and decode tildes in parameter values as ~~", function () { + var matcher1 = new UrlMatcher('/:foo'); + + expect(matcher1.format({ foo: "abc" })).toBe('/abc'); + expect(matcher1.format({ foo: "~abc" })).toBe('/~~abc'); + + expect(matcher1.exec('/abc').foo).toBe("abc"); + expect(matcher1.exec('/~~abc').foo).toBe("~abc"); }); describe("snake-case parameters", function() { @@ -306,6 +336,8 @@ describe("UrlMatcher", function () { $location.url("/foo"); expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } ); + $location.url("/foo?param1="); + expect(m.exec($location.path(), $location.search())).toEqual( { param1: undefined } ); $location.url("/foo?param1=bar"); expect(m.exec($location.path(), $location.search())).toEqual( { param1: [ 'bar' ] } ); $location.url("/foo?param1=bar¶m1=baz");