From 7e0288cd35040a8f0e3fa879e02ab8e04ed10290 Mon Sep 17 00:00:00 2001 From: Caitlin Potter Date: Thu, 6 Feb 2014 21:17:40 -0500 Subject: [PATCH] feat($location): parse query parameters delimited by ; or & In accordance with recomendation in http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2, the query parameters should support encoding and decoding using & or ; as a delimiter. Angular will consistently encode search queries using either '&' or ';' as the delimiter, with '&' being the default. This can be configured like so: ```js $locationProvider.queryDelimiter(';'); // any other value will be treated as '&' ``` Closes #6140 --- src/Angular.js | 9 ++++-- src/ng/location.js | 33 +++++++++++++++++---- test/ng/locationSpec.js | 63 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 9811c3637eba..f4054037110b 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1021,7 +1021,7 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ + forEach((keyValue || "").split(/[&;]/), function(keyValue){ if ( keyValue ) { key_value = keyValue.split('='); key = tryDecodeURIComponent(key_value[0]); @@ -1040,8 +1040,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) { @@ -1053,7 +1056,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 9ab99cb719fa..20df6a3d8ec2 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -87,7 +87,7 @@ 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 || ''; var appBaseNoFile = stripFile(appBase); @@ -120,7 +120,7 @@ function LocationHtml5Url(appBase, basePrefix) { * @private */ this.$$compose = function() { - var search = toKeyValue(this.$$search), + var search = toKeyValue(this.$$search, queryDelimiter), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; @@ -155,7 +155,7 @@ 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); parseAbsoluteUrl(appBase, this, appBase); @@ -227,7 +227,7 @@ function LocationHashbangUrl(appBase, hashPrefix) { * @private */ this.$$compose = function() { - var search = toKeyValue(this.$$search), + var search = toKeyValue(this.$$search, queryDelimiter), hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; @@ -531,7 +531,8 @@ function locationGetterSetter(property, preprocess) { */ function $LocationProvider(){ var hashPrefix = '', - html5Mode = false; + html5Mode = false, + queryDelimiter = '&'; /** * @ngdoc property @@ -567,6 +568,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 ng.$location#$locationChangeStart @@ -611,7 +632,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..912e753e5de5 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -313,6 +313,38 @@ 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 LocationHtml5Url('http://host.com/'); + url.$$parse('http://host.com/?foo=1&bar=2;baz=3'); + expect(url.search()).toEqual({ + 'foo': '1', + 'bar': '2', + 'baz': '3' + }); + }); + + + 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 +467,17 @@ 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=3'); + expect(url.search()).toEqual({ + 'foo': '1', + 'bar': '2', + 'baz': '3' + }); + }); + + 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 +507,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$/); + }); }); });