Skip to content

Commit 2c5afdb

Browse files
author
Sebastien Armand - sa250111
committed
feat($location): parse query parameters delimited by ; or &
According to RFC (http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.2.2) location should match either ';' OR '&', but not both at the same time. Closes angular#6140
2 parents cbcfaa2 + 1073967 commit 2c5afdb

File tree

3 files changed

+120
-12
lines changed

3 files changed

+120
-12
lines changed

src/Angular.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1055,9 +1055,10 @@ function tryDecodeURIComponent(value) {
10551055
* Parses an escaped url query string into key-value pairs.
10561056
* @returns Object.<(string|boolean)>
10571057
*/
1058-
function parseKeyValue(/**string*/keyValue) {
1058+
function parseKeyValue(/**string*/keyValue, delimiter) {
1059+
delimiter = delimiter === ';' ? delimiter : '&';
10591060
var obj = {}, key_value, key;
1060-
forEach((keyValue || "").split('&'), function(keyValue){
1061+
forEach((keyValue || "").split(delimiter), function(keyValue){
10611062
if ( keyValue ) {
10621063
key_value = keyValue.split('=');
10631064
key = tryDecodeURIComponent(key_value[0]);
@@ -1076,8 +1077,11 @@ function parseKeyValue(/**string*/keyValue) {
10761077
return obj;
10771078
}
10781079

1079-
function toKeyValue(obj) {
1080+
function toKeyValue(obj, delimiter) {
10801081
var parts = [];
1082+
if (delimiter !== '&' && delimiter !== ';') {
1083+
delimiter = '&';
1084+
}
10811085
forEach(obj, function(value, key) {
10821086
if (isArray(value)) {
10831087
forEach(value, function(arrayValue) {
@@ -1089,7 +1093,7 @@ function toKeyValue(obj) {
10891093
(value === true ? '' : '=' + encodeUriQuery(value, true)));
10901094
}
10911095
});
1092-
return parts.length ? parts.join('&') : '';
1096+
return parts.length ? parts.join(delimiter) : '';
10931097
}
10941098

10951099

src/ng/location.js

+37-8
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function parseAppUrl(relativeUrl, locationObj, appBase) {
3939
var match = urlResolve(relativeUrl, appBase);
4040
locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ?
4141
match.pathname.substring(1) : match.pathname);
42-
locationObj.$$search = parseKeyValue(match.search);
42+
locationObj.$$search = parseKeyValue(match.search, locationObj.$$queryDelimiter);
4343
locationObj.$$hash = decodeURIComponent(match.hash);
4444

4545
// make sure path starts with '/';
@@ -87,9 +87,10 @@ 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 || '';
93+
this.$$queryDelimiter = queryDelimiter;
9394
var appBaseNoFile = stripFile(appBase);
9495
parseAbsoluteUrl(appBase, this, appBase);
9596

@@ -120,7 +121,7 @@ function LocationHtml5Url(appBase, basePrefix) {
120121
* @private
121122
*/
122123
this.$$compose = function() {
123-
var search = toKeyValue(this.$$search),
124+
var search = toKeyValue(this.$$search, this.$$queryDelimiter),
124125
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
125126

126127
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
@@ -155,8 +156,9 @@ function LocationHtml5Url(appBase, basePrefix) {
155156
* @param {string} appBase application base URL
156157
* @param {string} hashPrefix hashbang prefix
157158
*/
158-
function LocationHashbangUrl(appBase, hashPrefix) {
159+
function LocationHashbangUrl(appBase, hashPrefix, queryDelimiter) {
159160
var appBaseNoFile = stripFile(appBase);
161+
this.$$queryDelimiter = queryDelimiter;
160162

161163
parseAbsoluteUrl(appBase, this, appBase);
162164

@@ -227,7 +229,7 @@ function LocationHashbangUrl(appBase, hashPrefix) {
227229
* @private
228230
*/
229231
this.$$compose = function() {
230-
var search = toKeyValue(this.$$search),
232+
var search = toKeyValue(this.$$search, this.$$queryDelimiter),
231233
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
232234

233235
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
@@ -287,6 +289,12 @@ LocationHashbangInHtml5Url.prototype =
287289
*/
288290
$$replace: false,
289291

292+
/**
293+
* Allows using ";" instead of "&" to separate query string arguments
294+
* @private
295+
*/
296+
$$queryDelimiter: '&',
297+
290298
/**
291299
* @ngdoc method
292300
* @name $location#absUrl
@@ -415,7 +423,7 @@ LocationHashbangInHtml5Url.prototype =
415423
return this.$$search;
416424
case 1:
417425
if (isString(search)) {
418-
this.$$search = parseKeyValue(search);
426+
this.$$search = parseKeyValue(search, this.$$queryDelimiter);
419427
} else if (isObject(search)) {
420428
this.$$search = search;
421429
} else {
@@ -520,7 +528,8 @@ function locationGetterSetter(property, preprocess) {
520528
*/
521529
function $LocationProvider(){
522530
var hashPrefix = '',
523-
html5Mode = false;
531+
html5Mode = false,
532+
queryDelimiter = '&';
524533

525534
/**
526535
* @ngdoc property
@@ -554,6 +563,26 @@ function $LocationProvider(){
554563
}
555564
};
556565

566+
/**
567+
* @ngdoc property
568+
* @name ng.$locationProvider#queryDelimiter
569+
* @methodOf ng.$locationProvider
570+
* @description
571+
* @param {string=} delimiter String to use as a delimiter for query parameters. Must be '&' or
572+
* ';'
573+
* @returns {*} current value if used as getter or itself (chaining) if used as setter
574+
*/
575+
this.queryDelimiter = function(delimiter) {
576+
if (arguments.length > 0) {
577+
if (delimiter !== ';' && delimiter !== '&') {
578+
delimiter = '&';
579+
}
580+
queryDelimiter = delimiter;
581+
return this;
582+
}
583+
return queryDelimiter;
584+
};
585+
557586
/**
558587
* @ngdoc event
559588
* @name $location#$locationChangeStart
@@ -596,7 +625,7 @@ function $LocationProvider(){
596625
appBase = stripHash(initialUrl);
597626
LocationMode = LocationHashbangUrl;
598627
}
599-
$location = new LocationMode(appBase, '#' + hashPrefix);
628+
$location = new LocationMode(appBase, '#' + hashPrefix, queryDelimiter);
600629
$location.$$parse($location.$$rewrite(initialUrl));
601630

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

test/ng/locationSpec.js

+75
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,44 @@ 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 LocationHashbangUrl('http://host.com/', '#');
320+
url.$$parse('http://host.com/#?foo=1&bar=2;baz');
321+
expect(url.search()).toEqual({
322+
'foo': '1',
323+
'bar': '2;baz'
324+
});
325+
326+
url = new LocationHashbangUrl('http://host.com/', '#', ';');
327+
url.$$parse('http://host.com/#?foo=1&bar=2;baz');
328+
expect(url.search()).toEqual({
329+
'foo': '1&bar',
330+
'baz': true
331+
});
332+
});
333+
334+
335+
it('should honor configured query param delimiter if ; --- otherwise use &', function() {
336+
url = new LocationHtml5Url('http://host.com/', '#', ';');
337+
url.$$parse('http://host.com/');
338+
url.search({
339+
"foo": "1",
340+
"bar": "2",
341+
"baz": "3"
342+
});
343+
expect(url.absUrl()).toMatch(/\?foo=1;bar=2;baz=3$/);
344+
345+
url = new LocationHtml5Url('http://host.com/', '#', '*');
346+
url.$$parse('http://host.com/');
347+
url.search({
348+
"foo": "1",
349+
"bar": "2",
350+
"baz": "3"
351+
});
352+
expect(url.absUrl()).toMatch(/\?foo=1&bar=2&baz=3$/);
353+
});
316354
});
317355
});
318356

@@ -435,6 +473,23 @@ describe('$location', function() {
435473
});
436474

437475

476+
it('should decode query params delimited interchangeably by & and ;', function() {
477+
var url = new LocationHashbangUrl('http://host.com/', '#');
478+
url.$$parse('http://host.com/#?foo=1&bar=2;baz');
479+
expect(url.search()).toEqual({
480+
'foo': '1',
481+
'bar': '2;baz'
482+
});
483+
484+
url = new LocationHashbangUrl('http://host.com/', '#', ';');
485+
url.$$parse('http://host.com/#?foo=1&bar=2;baz');
486+
expect(url.search()).toEqual({
487+
'foo': '1&bar',
488+
'baz': true
489+
});
490+
});
491+
492+
438493
it('should return decoded characters for search specified with setter', function() {
439494
var locationUrl = new LocationHtml5Url('http://host.com/');
440495
locationUrl.$$parse('http://host.com/')
@@ -464,6 +519,26 @@ describe('$location', function() {
464519
locationUrl.search({'q': '4/5 6'});
465520
expect(locationUrl.absUrl()).toEqual('http://host.com?q=4%2F5%206');
466521
});
522+
523+
it('should honor configured query param delimiter if ; --- otherwise use &', function() {
524+
url = new LocationHashbangUrl('http://host.com/', '#', ';');
525+
url.$$parse('http://host.com/');
526+
url.search({
527+
"foo": "1",
528+
"bar": "2",
529+
"baz": "3"
530+
});
531+
expect(url.absUrl()).toMatch(/\?foo=1;bar=2;baz=3$/);
532+
533+
url = new LocationHashbangUrl('http://host.com/', '#', '*');
534+
url.$$parse('http://host.com/');
535+
url.search({
536+
"foo": "1",
537+
"bar": "2",
538+
"baz": "3"
539+
});
540+
expect(url.absUrl()).toMatch(/\?foo=1&bar=2&baz=3$/);
541+
});
467542
});
468543
});
469544

0 commit comments

Comments
 (0)