diff --git a/src/Angular.js b/src/Angular.js index 21595cd60c24..8a41af59cd7b 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1055,9 +1055,10 @@ function tryDecodeURIComponent(value) { * Parses an escaped url query string into key-value pairs. * @returns Object.<(string|boolean)> */ -function parseKeyValue(/**string*/keyValue) { +function parseKeyValue(/**string*/keyValue, delimiter) { + delimiter = delimiter === ';' ? delimiter : '&'; var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ + forEach((keyValue || "").split(delimiter), function(keyValue){ if ( keyValue ) { key_value = keyValue.split('='); key = tryDecodeURIComponent(key_value[0]); @@ -1076,8 +1077,11 @@ function parseKeyValue(/**string*/keyValue) { return obj; } -function toKeyValue(obj) { +function toKeyValue(obj, delimiter) { var parts = []; + if (delimiter !== '&' && delimiter !== ';') { + delimiter = '&'; + } forEach(obj, function(value, key) { if (isArray(value)) { forEach(value, function(arrayValue) { @@ -1089,7 +1093,7 @@ function toKeyValue(obj) { (value === true ? '' : '=' + encodeUriQuery(value, true))); } }); - return parts.length ? parts.join('&') : ''; + return parts.length ? parts.join(delimiter) : ''; } diff --git a/src/ng/location.js b/src/ng/location.js index c4b7545d78f0..94ca46a2d145 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -39,7 +39,7 @@ function parseAppUrl(relativeUrl, locationObj, appBase) { var match = urlResolve(relativeUrl, appBase); locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); - locationObj.$$search = parseKeyValue(match.search); + locationObj.$$search = parseKeyValue(match.search, locationObj.$$queryDelimiter); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; @@ -87,9 +87,10 @@ function serverBase(url) { * @param {string} appBase application base URL * @param {string} basePrefix url path prefix */ -function LocationHtml5Url(appBase, basePrefix) { +function LocationHtml5Url(appBase, basePrefix, queryDelimiter) { this.$$html5 = true; basePrefix = basePrefix || ''; + this.$$queryDelimiter = queryDelimiter; var appBaseNoFile = stripFile(appBase); parseAbsoluteUrl(appBase, this, appBase); @@ -120,7 +121,7 @@ function LocationHtml5Url(appBase, basePrefix) { * @private */ this.$$compose = function() { - var search = toKeyValue(this.$$search), + var search = toKeyValue(this.$$search, this.$$queryDelimiter), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; @@ -155,8 +156,9 @@ function LocationHtml5Url(appBase, basePrefix) { * @param {string} appBase application base URL * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangUrl(appBase, hashPrefix) { +function LocationHashbangUrl(appBase, hashPrefix, queryDelimiter) { var appBaseNoFile = stripFile(appBase); + this.$$queryDelimiter = queryDelimiter; parseAbsoluteUrl(appBase, this, appBase); @@ -227,7 +229,7 @@ function LocationHashbangUrl(appBase, hashPrefix) { * @private */ this.$$compose = function() { - var search = toKeyValue(this.$$search), + var search = toKeyValue(this.$$search, this.$$queryDelimiter), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; @@ -287,6 +289,12 @@ LocationHashbangInHtml5Url.prototype = */ $$replace: false, + /** + * Allows using ";" instead of "&" to separate query string arguments + * @private + */ + $$queryDelimiter: '&', + /** * @ngdoc method * @name $location#absUrl @@ -415,7 +423,7 @@ LocationHashbangInHtml5Url.prototype = return this.$$search; case 1: if (isString(search)) { - this.$$search = parseKeyValue(search); + this.$$search = parseKeyValue(search, this.$$queryDelimiter); } else if (isObject(search)) { this.$$search = search; } else { @@ -520,7 +528,8 @@ function locationGetterSetter(property, preprocess) { */ function $LocationProvider(){ var hashPrefix = '', - html5Mode = false; + html5Mode = false, + queryDelimiter = '&'; /** * @ngdoc property @@ -554,6 +563,26 @@ function $LocationProvider(){ } }; + /** + * @ngdoc property + * @name ng.$locationProvider#queryDelimiter + * @methodOf ng.$locationProvider + * @description + * @param {string=} delimiter String to use as a delimiter for query parameters. Must be '&' or + * ';' + * @returns {*} current value if used as getter or itself (chaining) if used as setter + */ + this.queryDelimiter = function(delimiter) { + if (arguments.length > 0) { + if (delimiter !== ';' && delimiter !== '&') { + delimiter = '&'; + } + queryDelimiter = delimiter; + return this; + } + return queryDelimiter; + }; + /** * @ngdoc event * @name $location#$locationChangeStart @@ -596,7 +625,7 @@ function $LocationProvider(){ appBase = stripHash(initialUrl); LocationMode = LocationHashbangUrl; } - $location = new LocationMode(appBase, '#' + hashPrefix); + $location = new LocationMode(appBase, '#' + hashPrefix, queryDelimiter); $location.$$parse($location.$$rewrite(initialUrl)); $rootElement.on('click', function(event) { diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index ff823d306efd..add3a2578aaf 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -313,6 +313,44 @@ describe('$location', function() { expect(url.search()).toEqual({'i j': '<>#'}); expect(url.hash()).toBe('x <>#'); }); + + + it('should decode query params delimited interchangeably by & and ;', function() { + var url = new LocationHashbangUrl('http://host.com/', '#'); + url.$$parse('http://host.com/#?foo=1&bar=2;baz'); + expect(url.search()).toEqual({ + 'foo': '1', + 'bar': '2;baz' + }); + + url = new LocationHashbangUrl('http://host.com/', '#', ';'); + url.$$parse('http://host.com/#?foo=1&bar=2;baz'); + expect(url.search()).toEqual({ + 'foo': '1&bar', + 'baz': true + }); + }); + + + it('should honor configured query param delimiter if ; --- otherwise use &', function() { + url = new LocationHtml5Url('http://host.com/', '#', ';'); + url.$$parse('http://host.com/'); + url.search({ + "foo": "1", + "bar": "2", + "baz": "3" + }); + expect(url.absUrl()).toMatch(/\?foo=1;bar=2;baz=3$/); + + url = new LocationHtml5Url('http://host.com/', '#', '*'); + url.$$parse('http://host.com/'); + url.search({ + "foo": "1", + "bar": "2", + "baz": "3" + }); + expect(url.absUrl()).toMatch(/\?foo=1&bar=2&baz=3$/); + }); }); }); @@ -435,6 +473,23 @@ describe('$location', function() { }); + it('should decode query params delimited interchangeably by & and ;', function() { + var url = new LocationHashbangUrl('http://host.com/', '#'); + url.$$parse('http://host.com/#?foo=1&bar=2;baz'); + expect(url.search()).toEqual({ + 'foo': '1', + 'bar': '2;baz' + }); + + url = new LocationHashbangUrl('http://host.com/', '#', ';'); + url.$$parse('http://host.com/#?foo=1&bar=2;baz'); + expect(url.search()).toEqual({ + 'foo': '1&bar', + 'baz': true + }); + }); + + it('should return decoded characters for search specified with setter', function() { var locationUrl = new LocationHtml5Url('http://host.com/'); locationUrl.$$parse('http://host.com/') @@ -464,6 +519,26 @@ describe('$location', function() { locationUrl.search({'q': '4/5 6'}); expect(locationUrl.absUrl()).toEqual('http://host.com?q=4%2F5%206'); }); + + it('should honor configured query param delimiter if ; --- otherwise use &', function() { + url = new LocationHashbangUrl('http://host.com/', '#', ';'); + url.$$parse('http://host.com/'); + url.search({ + "foo": "1", + "bar": "2", + "baz": "3" + }); + expect(url.absUrl()).toMatch(/\?foo=1;bar=2;baz=3$/); + + url = new LocationHashbangUrl('http://host.com/', '#', '*'); + url.$$parse('http://host.com/'); + url.search({ + "foo": "1", + "bar": "2", + "baz": "3" + }); + expect(url.absUrl()).toMatch(/\?foo=1&bar=2&baz=3$/); + }); }); });