diff options
| author | joshkurz | 2013-04-26 17:31:56 -0400 | 
|---|---|---|
| committer | Pete Bacon Darwin | 2013-07-01 19:32:12 +0100 | 
| commit | 807394095b991357225a03d5fed81fea5c9a1abe (patch) | |
| tree | 5e2d1a6eb337f2fcb7dfc7a7bc76a271f6ae7af5 | |
| parent | a258817310e83ae58a0ce95226e77a9f151d7197 (diff) | |
| download | angular.js-807394095b991357225a03d5fed81fea5c9a1abe.tar.bz2 | |
fix(Angular.js): handle duplicate params in parseKeyValue/toKeyValue
parseKeyValue and toKeyValue can now handle duplicate values in the query.
```
?x=1&x=2 <-> {x:[1,2]}
```
The algorithm looks like:
    1)parseKeyValue looks for presence of obj[key]
    2)detects and replaces obj[key] with [obj[key],val]
    3)then pushes more duplicates if necessary
    4)toKeyValue decodes array correctly
    5)(not changed)$location.search({param: 'key'}) still replaces if necessary
    6)(not changed)$location.search({param: ['key1', 'key2']}) sets the url with duplicates
BREAKING CHANGE: Before this change:
- `parseKeyValue` only took the last key overwriting all the previous keys;
- `toKeyValue` joined the keys together in a comma delimited string.
This was deemed buggy behavior. If your server relied on this behavior
then either the server should be fixed or a simple serialization of
the array should be done on the client before passing it to $location.
| -rw-r--r-- | src/Angular.js | 15 | ||||
| -rw-r--r-- | src/ng/location.js | 3 | ||||
| -rw-r--r-- | test/AngularSpec.js | 21 | ||||
| -rw-r--r-- | test/ng/locationSpec.js | 23 | 
4 files changed, 58 insertions, 4 deletions
| diff --git a/src/Angular.js b/src/Angular.js index 5caf4665..c1066618 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -850,7 +850,14 @@ function parseKeyValue(/**string*/keyValue) {        key_value = keyValue.split('=');        key = tryDecodeURIComponent(key_value[0]);        if ( isDefined(key) ) { -        obj[key] = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; +        var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; +        if (!obj[key]) { +          obj[key] = val; +        } else if(isArray(obj[key])) { +          obj[key].push(val); +        } else { +          obj[key] = [obj[key],val]; +        }        }      }    }); @@ -860,7 +867,13 @@ function parseKeyValue(/**string*/keyValue) {  function toKeyValue(obj) {    var parts = [];    forEach(obj, function(value, key) { +    if (isArray(value)) { +      forEach(value, function(arrayValue) { +        parts.push(encodeUriQuery(key, true) + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); +      }); +    } else {      parts.push(encodeUriQuery(key, true) + (value === true ? '' : '=' + encodeUriQuery(value, true))); +    }    });    return parts.length ? parts.join('&') : '';  } diff --git a/src/ng/location.js b/src/ng/location.js index 40d39282..a0f03a8e 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -344,7 +344,8 @@ LocationHashbangInHtml5Url.prototype =     *     * Change search part when called with parameter and return `$location`.     * -   * @param {string|object<string,string>=} search New search params - string or hash object +   * @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or hash object. Hash object +   *    may contain an array of values, which will be decoded as duplicates in the url.     * @param {string=} paramValue If `search` is a string, then `paramValue` will override only a     *    single search parameter. If the value is `null`, the parameter will be deleted.     * diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 6384302c..9686ffd6 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -318,10 +318,21 @@ describe('angular', function() {        expect(parseKeyValue('invalid=%')).toEqual({ invalid: undefined });        expect(parseKeyValue('invalid=%&valid=good')).toEqual({ invalid: undefined, valid: 'good' });      }); +    it('should parse a string into key-value pairs with duplicates grouped in an array', function() { +      expect(parseKeyValue('')).toEqual({}); +      expect(parseKeyValue('duplicate=pair')).toEqual({duplicate: 'pair'}); +      expect(parseKeyValue('first=1&first=2')).toEqual({first: ['1','2']}); +      expect(parseKeyValue('escaped%20key=escaped%20value&&escaped%20key=escaped%20value2')). +      toEqual({'escaped key': ['escaped value','escaped value2']}); +      expect(parseKeyValue('flag1&key=value&flag1')). +      toEqual({flag1: [true,true], key: 'value'}); +      expect(parseKeyValue('flag1&flag1=value&flag1=value2&flag1')). +      toEqual({flag1: [true,'value','value2',true]}); +    });    });    describe('toKeyValue', function() { -    it('should parse key-value pairs into string', function() { +    it('should serialize key-value pairs into string', function() {        expect(toKeyValue({})).toEqual('');        expect(toKeyValue({simple: 'pair'})).toEqual('simple=pair');        expect(toKeyValue({first: '1', second: '2'})).toEqual('first=1&second=2'); @@ -330,9 +341,15 @@ describe('angular', function() {        expect(toKeyValue({emptyKey: ''})).toEqual('emptyKey=');      }); -    it('should parse true values into flags', function() { +    it('should serialize true values into flags', function() {        expect(toKeyValue({flag1: true, key: 'value', flag2: true})).toEqual('flag1&key=value&flag2');      }); + +    it('should serialize duplicates into duplicate param strings', function() { +      expect(toKeyValue({key: [323,'value',true]})).toEqual('key=323&key=value&key'); +      expect(toKeyValue({key: [323,'value',true, 1234]})). +      toEqual('key=323&key=value&key&key=1234'); +  });    }); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index df8043a3..df826525 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -368,6 +368,29 @@ describe('$location', function() {          locationUrl.search('q', '1/2 3');          expect(locationUrl.search()).toEqual({'q': '1/2 3'});        }); + +      it('should return an array for duplicate params', function() { +        var locationUrl = new LocationHtml5Url('http://host.com'); +        locationUrl.$$parse('http://host.com') +        locationUrl.search('q', ['1/2 3','4/5 6']); +        expect(locationUrl.search()).toEqual({'q': ['1/2 3','4/5 6']}); +      }); + +      it('should encode an array correctly from search and add to url', function() { +        var locationUrl = new LocationHtml5Url('http://host.com'); +        locationUrl.$$parse('http://host.com') +        locationUrl.search({'q': ['1/2 3','4/5 6']}); +        expect(locationUrl.absUrl()).toEqual('http://host.com?q=1%2F2%203&q=4%2F5%206'); +      }); + +      it('should rewrite params when specifing a single param in search', function() { +        var locationUrl = new LocationHtml5Url('http://host.com'); +        locationUrl.$$parse('http://host.com') +        locationUrl.search({'q': '1/2 3'}); +        expect(locationUrl.absUrl()).toEqual('http://host.com?q=1%2F2%203'); +        locationUrl.search({'q': '4/5 6'}); +        expect(locationUrl.absUrl()).toEqual('http://host.com?q=4%2F5%206'); +      });      });    }); | 
