Skip to content

Commit 7e0288c

Browse files
committed
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 angular#6140
1 parent 95be253 commit 7e0288c

File tree

3 files changed

+96
-9
lines changed

3 files changed

+96
-9
lines changed

src/Angular.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ function tryDecodeURIComponent(value) {
10211021
*/
10221022
function parseKeyValue(/**string*/keyValue) {
10231023
var obj = {}, key_value, key;
1024-
forEach((keyValue || "").split('&'), function(keyValue){
1024+
forEach((keyValue || "").split(/[&;]/), function(keyValue){
10251025
if ( keyValue ) {
10261026
key_value = keyValue.split('=');
10271027
key = tryDecodeURIComponent(key_value[0]);
@@ -1040,8 +1040,11 @@ function parseKeyValue(/**string*/keyValue) {
10401040
return obj;
10411041
}
10421042

1043-
function toKeyValue(obj) {
1043+
function toKeyValue(obj, delimiter) {
10441044
var parts = [];
1045+
if (delimiter !== '&' && delimiter !== ';') {
1046+
delimiter = '&';
1047+
}
10451048
forEach(obj, function(value, key) {
10461049
if (isArray(value)) {
10471050
forEach(value, function(arrayValue) {
@@ -1053,7 +1056,7 @@ function toKeyValue(obj) {
10531056
(value === true ? '' : '=' + encodeUriQuery(value, true)));
10541057
}
10551058
});
1056-
return parts.length ? parts.join('&') : '';
1059+
return parts.length ? parts.join(delimiter) : '';
10571060
}
10581061

10591062

src/ng/location.js

+27-6
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function serverBase(url) {
8787
* @param {string} appBase application base URL
8888
* @param {string} basePrefix url path prefix
8989
*/
90-
function LocationHtml5Url(appBase, basePrefix) {
90+
function LocationHtml5Url(appBase, basePrefix, queryDelimiter) {
9191
this.$$html5 = true;
9292
basePrefix = basePrefix || '';
9393
var appBaseNoFile = stripFile(appBase);
@@ -120,7 +120,7 @@ function LocationHtml5Url(appBase, basePrefix) {
120120
* @private
121121
*/
122122
this.$$compose = function() {
123-
var search = toKeyValue(this.$$search),
123+
var search = toKeyValue(this.$$search, queryDelimiter),
124124
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
125125

126126
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
@@ -155,7 +155,7 @@ function LocationHtml5Url(appBase, basePrefix) {
155155
* @param {string} appBase application base URL
156156
* @param {string} hashPrefix hashbang prefix
157157
*/
158-
function LocationHashbangUrl(appBase, hashPrefix) {
158+
function LocationHashbangUrl(appBase, hashPrefix, queryDelimiter) {
159159
var appBaseNoFile = stripFile(appBase);
160160

161161
parseAbsoluteUrl(appBase, this, appBase);
@@ -227,7 +227,7 @@ function LocationHashbangUrl(appBase, hashPrefix) {
227227
* @private
228228
*/
229229
this.$$compose = function() {
230-
var search = toKeyValue(this.$$search),
230+
var search = toKeyValue(this.$$search, queryDelimiter),
231231
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
232232

233233
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
@@ -531,7 +531,8 @@ function locationGetterSetter(property, preprocess) {
531531
*/
532532
function $LocationProvider(){
533533
var hashPrefix = '',
534-
html5Mode = false;
534+
html5Mode = false,
535+
queryDelimiter = '&';
535536

536537
/**
537538
* @ngdoc property
@@ -567,6 +568,26 @@ function $LocationProvider(){
567568
}
568569
};
569570

571+
/**
572+
* @ngdoc property
573+
* @name ng.$locationProvider#queryDelimiter
574+
* @methodOf ng.$locationProvider
575+
* @description
576+
* @param {string=} delimiter String to use as a delimiter for query parameters. Must be '&' or
577+
* ';'
578+
* @returns {*} current value if used as getter or itself (chaining) if used as setter
579+
*/
580+
this.queryDelimiter = function(delimiter) {
581+
if (arguments.length > 0) {
582+
if (delimiter !== ';' && delimiter !== '&') {
583+
delimiter = '&';
584+
}
585+
queryDelimiter = delimiter;
586+
return this;
587+
}
588+
return queryDelimiter;
589+
};
590+
570591
/**
571592
* @ngdoc event
572593
* @name ng.$location#$locationChangeStart
@@ -611,7 +632,7 @@ function $LocationProvider(){
611632
appBase = stripHash(initialUrl);
612633
LocationMode = LocationHashbangUrl;
613634
}
614-
$location = new LocationMode(appBase, '#' + hashPrefix);
635+
$location = new LocationMode(appBase, '#' + hashPrefix, queryDelimiter);
615636
$location.$$parse($location.$$rewrite(initialUrl));
616637

617638
$rootElement.on('click', function(event) {

test/ng/locationSpec.js

+63
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,38 @@ describe('$location', function() {
313313
expect(url.search()).toEqual({'i j': '<>#'});
314314
expect(url.hash()).toBe('x <>#');
315315
});
316+
317+
318+
it('should decode query params delimited interchangeably by & and ;', function() {
319+
var url = new LocationHtml5Url('http://host.com/');
320+
url.$$parse('http://host.com/?foo=1&bar=2;baz=3');
321+
expect(url.search()).toEqual({
322+
'foo': '1',
323+
'bar': '2',
324+
'baz': '3'
325+
});
326+
});
327+
328+
329+
it('should honor configured query param delimiter if ; --- otherwise use &', function() {
330+
url = new LocationHtml5Url('http://host.com/', '#', ';');
331+
url.$$parse('http://host.com/');
332+
url.search({
333+
"foo": "1",
334+
"bar": "2",
335+
"baz": "3"
336+
});
337+
expect(url.absUrl()).toMatch(/\?foo=1;bar=2;baz=3$/);
338+
339+
url = new LocationHtml5Url('http://host.com/', '#', '*');
340+
url.$$parse('http://host.com/');
341+
url.search({
342+
"foo": "1",
343+
"bar": "2",
344+
"baz": "3"
345+
});
346+
expect(url.absUrl()).toMatch(/\?foo=1&bar=2&baz=3$/);
347+
});
316348
});
317349
});
318350

@@ -435,6 +467,17 @@ describe('$location', function() {
435467
});
436468

437469

470+
it('should decode query params delimited interchangeably by & and ;', function() {
471+
var url = new LocationHashbangUrl('http://host.com/', '#');
472+
url.$$parse('http://host.com/#?foo=1&bar=2;baz=3');
473+
expect(url.search()).toEqual({
474+
'foo': '1',
475+
'bar': '2',
476+
'baz': '3'
477+
});
478+
});
479+
480+
438481
it('should return decoded characters for search specified with setter', function() {
439482
var locationUrl = new LocationHtml5Url('http://host.com/');
440483
locationUrl.$$parse('http://host.com/')
@@ -464,6 +507,26 @@ describe('$location', function() {
464507
locationUrl.search({'q': '4/5 6'});
465508
expect(locationUrl.absUrl()).toEqual('http://host.com?q=4%2F5%206');
466509
});
510+
511+
it('should honor configured query param delimiter if ; --- otherwise use &', function() {
512+
url = new LocationHashbangUrl('http://host.com/', '#', ';');
513+
url.$$parse('http://host.com/');
514+
url.search({
515+
"foo": "1",
516+
"bar": "2",
517+
"baz": "3"
518+
});
519+
expect(url.absUrl()).toMatch(/\?foo=1;bar=2;baz=3$/);
520+
521+
url = new LocationHashbangUrl('http://host.com/', '#', '*');
522+
url.$$parse('http://host.com/');
523+
url.search({
524+
"foo": "1",
525+
"bar": "2",
526+
"baz": "3"
527+
});
528+
expect(url.absUrl()).toMatch(/\?foo=1&bar=2&baz=3$/);
529+
});
467530
});
468531
});
469532

0 commit comments

Comments
 (0)