qs 2.4.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ dist
package/.npmignore CHANGED
@@ -16,3 +16,4 @@ config.json
16
16
  coverage.*
17
17
  lib-cov
18
18
  complexity.md
19
+ dist
package/CHANGELOG.md CHANGED
@@ -1,13 +1,34 @@
1
1
 
2
- ## [**2.3.3**](https://github.com/hapijs/qs/issues?milestone=18&state=open)
3
- - [**#59**](https://github.com/hapijs/qs/issues/59) make sure array indexes are >= 0, closes #57
2
+ ## [**3.1.0**](https://github.com/hapijs/qs/issues?milestone=24&state=open)
3
+ - [**#89**](https://github.com/hapijs/qs/issues/89) Add option to disable "Transform dot notation to bracket notation"
4
+
5
+ ## [**3.0.0**](https://github.com/hapijs/qs/issues?milestone=23&state=closed)
6
+ - [**#77**](https://github.com/hapijs/qs/issues/77) Perf boost
7
+ - [**#60**](https://github.com/hapijs/qs/issues/60) Add explicit option to disable array parsing
8
+ - [**#80**](https://github.com/hapijs/qs/issues/80) qs.parse silently drops properties
9
+ - [**#74**](https://github.com/hapijs/qs/issues/74) Bad parse when turning array into object
10
+ - [**#81**](https://github.com/hapijs/qs/issues/81) Add a `filter` option
11
+ - [**#68**](https://github.com/hapijs/qs/issues/68) Fixed issue with recursion and passing strings into objects.
12
+ - [**#66**](https://github.com/hapijs/qs/issues/66) Add mixed array and object dot notation support Closes: #47
13
+ - [**#76**](https://github.com/hapijs/qs/issues/76) RFC 3986
14
+ - [**#85**](https://github.com/hapijs/qs/issues/85) No equal sign
15
+ - [**#84**](https://github.com/hapijs/qs/issues/84) update license attribute
16
+
17
+ ## [**2.4.1**](https://github.com/hapijs/qs/issues?milestone=20&state=closed)
18
+ - [**#73**](https://github.com/hapijs/qs/issues/73) Property 'hasOwnProperty' of object #<Object> is not a function
19
+
20
+ ## [**2.4.0**](https://github.com/hapijs/qs/issues?milestone=19&state=closed)
21
+ - [**#70**](https://github.com/hapijs/qs/issues/70) Add arrayFormat option
22
+
23
+ ## [**2.3.3**](https://github.com/hapijs/qs/issues?milestone=18&state=closed)
24
+ - [**#59**](https://github.com/hapijs/qs/issues/59) make sure array indexes are >= 0, closes #57
4
25
  - [**#58**](https://github.com/hapijs/qs/issues/58) make qs usable for browser loader
5
26
 
6
27
  ## [**2.3.2**](https://github.com/hapijs/qs/issues?milestone=17&state=closed)
7
28
  - [**#55**](https://github.com/hapijs/qs/issues/55) allow merging a string into an object
8
29
 
9
30
  ## [**2.3.1**](https://github.com/hapijs/qs/issues?milestone=16&state=closed)
10
- - [**#52**](https://github.com/hapijs/qs/issues/52) Return &quot;undefined&quot; and &quot;false&quot; instead of throwing &quot;TypeError&quot;.
31
+ - [**#52**](https://github.com/hapijs/qs/issues/52) Return "undefined" and "false" instead of throwing "TypeError".
11
32
 
12
33
  ## [**2.3.0**](https://github.com/hapijs/qs/issues?milestone=15&state=closed)
13
34
  - [**#50**](https://github.com/hapijs/qs/issues/50) add option to omit array indices, closes #46
@@ -34,9 +55,9 @@
34
55
  - [**#31**](https://github.com/hapijs/qs/issues/31) qs.parse stackoverflow on circular objects
35
56
 
36
57
  ## [**2.2.0**](https://github.com/hapijs/qs/issues?milestone=9&state=closed)
37
- - [**#26**](https://github.com/hapijs/qs/issues/26) Don&#39;t use Buffer global if it&#39;s not present
58
+ - [**#26**](https://github.com/hapijs/qs/issues/26) Don't use Buffer global if it's not present
38
59
  - [**#30**](https://github.com/hapijs/qs/issues/30) Bug when merging non-object values into arrays
39
- - [**#29**](https://github.com/hapijs/qs/issues/29) Don&#39;t call Utils.clone at the top of Utils.merge
60
+ - [**#29**](https://github.com/hapijs/qs/issues/29) Don't call Utils.clone at the top of Utils.merge
40
61
  - [**#23**](https://github.com/hapijs/qs/issues/23) Ability to not limit parameters?
41
62
 
42
63
  ## [**2.1.0**](https://github.com/hapijs/qs/issues?milestone=8&state=closed)
@@ -48,7 +69,7 @@
48
69
  - [**#21**](https://github.com/hapijs/qs/issues/21) make all limits optional, for #18, for #20
49
70
 
50
71
  ## [**1.2.2**](https://github.com/hapijs/qs/issues?milestone=6&state=closed)
51
- - [**#19**](https://github.com/hapijs/qs/issues/19) Don&#39;t overwrite null values
72
+ - [**#19**](https://github.com/hapijs/qs/issues/19) Don't overwrite null values
52
73
 
53
74
  ## [**1.2.1**](https://github.com/hapijs/qs/issues?milestone=5&state=closed)
54
75
  - [**#16**](https://github.com/hapijs/qs/issues/16) ignore non-string delimiters
@@ -65,4 +86,3 @@
65
86
 
66
87
  ## [**1.0.2**](https://github.com/hapijs/qs/issues?milestone=2&state=closed)
67
88
  - [**#5**](https://github.com/hapijs/qs/issues/5) array holes incorrectly copied into object on large index
68
-
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -23,7 +23,7 @@ var str = Qs.stringify(obj); // 'a=c'
23
23
  Qs.parse(string, [options]);
24
24
  ```
25
25
 
26
- **qs** allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets `[]`.
26
+ **qs** allows you to create nested objects within your query strings, by surrounding the name of sub-keys with square brackets `[]`, or prefixing the sub-key with a dot `.`.
27
27
  For example, the string `'foo[bar]=baz'` converts to:
28
28
 
29
29
  ```javascript
@@ -34,6 +34,20 @@ For example, the string `'foo[bar]=baz'` converts to:
34
34
  }
35
35
  ```
36
36
 
37
+ When using the `plainObjects` option the parsed value is returned as a plain object, created via `Object.create(null)` and as such you should be aware that prototype methods will not exist on it and a user may set those names to whatever value they like:
38
+
39
+ ```javascript
40
+ Qs.parse('a.hasOwnProperty=b', { plainObjects: true });
41
+ // { a: { hasOwnProperty: 'b' } }
42
+ ```
43
+
44
+ By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use `plainObjects` as mentioned above, or set `allowPrototypes` to `true` which will allow user input to overwrite those properties. *WARNING* It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. Always be careful with this option.
45
+
46
+ ```javascript
47
+ Qs.parse('a.hasOwnProperty=b', { allowPrototypes: true });
48
+ // { a: { hasOwnProperty: 'b' } }
49
+ ```
50
+
37
51
  URI encoded strings work too:
38
52
 
39
53
  ```javascript
@@ -104,6 +118,13 @@ Qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
104
118
  // { a: 'b', c: 'd', e: 'f' }
105
119
  ```
106
120
 
121
+ Option `allowDots` can be used to disable dot notation:
122
+
123
+ ```javascript
124
+ Qs.parse('a.b=c', { allowDots: false });
125
+ // { 'a.b': 'c' } }
126
+ ```
127
+
107
128
  ### Parsing Arrays
108
129
 
109
130
  **qs** can also parse arrays using a similar `[]` notation:
@@ -153,7 +174,12 @@ Qs.parse('a[1]=b', { arrayLimit: 0 });
153
174
  // { a: { '1': 'b' } }
154
175
  ```
155
176
 
156
- To disable array parsing entirely, set `arrayLimit` to `-1`.
177
+ To disable array parsing entirely, set `parseArrays` to `false`.
178
+
179
+ ```javascript
180
+ Qs.parse('a[]=b', { parseArrays: false });
181
+ // { a: { '0': 'b' } }
182
+ ```
157
183
 
158
184
  If you mix notations, **qs** will merge the two items into an object:
159
185
 
@@ -231,3 +257,61 @@ The delimiter may be overridden with stringify as well:
231
257
  Qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' });
232
258
  // 'a=b;c=d'
233
259
  ```
260
+
261
+ Finally, you can use the `filter` option to restrict which keys will be included in the stringified output.
262
+ If you pass a function, it will be called for each key to obtain the replacement value. Otherwise, if you
263
+ pass an array, it will be used to select properties and array indices for stringification:
264
+
265
+ ```javascript
266
+ function filterFunc(prefix, value) {
267
+ if (prefix == 'b') {
268
+ // Return an `undefined` value to omit a property.
269
+ return;
270
+ }
271
+ if (prefix == 'e[f]') {
272
+ return value.getTime();
273
+ }
274
+ if (prefix == 'e[g][0]') {
275
+ return value * 2;
276
+ }
277
+ return value;
278
+ }
279
+ Qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc })
280
+ // 'a=b&c=d&e[f]=123&e[g][0]=4'
281
+ Qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] })
282
+ // 'a=b&e=f'
283
+ Qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] })
284
+ // 'a[0]=b&a[2]=d'
285
+ ```
286
+
287
+ ### Handling of `null` values
288
+
289
+ By default, `null` values are treated like empty strings:
290
+
291
+ ```javascript
292
+ Qs.stringify({ a: null, b: '' });
293
+ // 'a=&b='
294
+ ```
295
+
296
+ Parsing does not distinguish between parameters with and without equal signs. Both are converted to empty strings.
297
+
298
+ ```javascript
299
+ Qs.parse('a&b=')
300
+ // { a: '', b: '' }
301
+ ```
302
+
303
+ To distinguish between `null` values and empty strings use the `strictNullHandling` flag. In the result string the `null`
304
+ values have no `=` sign:
305
+
306
+ ```javascript
307
+ Qs.stringify({ a: null, b: '' }, { strictNullHandling: true });
308
+ // 'a&b='
309
+ ```
310
+
311
+ To parse values without `=` back to `null` use the `strictNullHandling` flag:
312
+
313
+ ```javascript
314
+ Qs.parse('a&b=', { strictNullHandling: true });
315
+ // { a: null, b: '' }
316
+
317
+ ```
package/bower.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "qs",
3
+ "main": "dist/qs.js",
4
+ "version": "3.0.0",
5
+ "homepage": "https://github.com/hapijs/qs",
6
+ "authors": [
7
+ "Nathan LaFreniere <quitlahok@gmail.com>"
8
+ ],
9
+ "description": "A querystring parser that supports nesting and arrays, with a depth limit",
10
+ "keywords": [
11
+ "querystring",
12
+ "qs"
13
+ ],
14
+ "license": "BSD-3-Clause",
15
+ "ignore": [
16
+ "**/.*",
17
+ "node_modules",
18
+ "bower_components",
19
+ "test",
20
+ "tests"
21
+ ]
22
+ }
package/lib/index.js CHANGED
File without changes
package/lib/parse.js CHANGED
@@ -9,7 +9,10 @@ var internals = {
9
9
  delimiter: '&',
10
10
  depth: 5,
11
11
  arrayLimit: 20,
12
- parameterLimit: 1000
12
+ parameterLimit: 1000,
13
+ strictNullHandling: false,
14
+ plainObjects: false,
15
+ allowPrototypes: false
13
16
  };
14
17
 
15
18
 
@@ -24,16 +27,16 @@ internals.parseValues = function (str, options) {
24
27
 
25
28
  if (pos === -1) {
26
29
  obj[Utils.decode(part)] = '';
30
+
31
+ if (options.strictNullHandling) {
32
+ obj[Utils.decode(part)] = null;
33
+ }
27
34
  }
28
35
  else {
29
36
  var key = Utils.decode(part.slice(0, pos));
30
37
  var val = Utils.decode(part.slice(pos + 1));
31
38
 
32
- if (Object.prototype.hasOwnProperty(key)) {
33
- continue;
34
- }
35
-
36
- if (!obj.hasOwnProperty(key)) {
39
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
37
40
  obj[key] = val;
38
41
  }
39
42
  else {
@@ -54,12 +57,13 @@ internals.parseObject = function (chain, val, options) {
54
57
 
55
58
  var root = chain.shift();
56
59
 
57
- var obj = {};
60
+ var obj;
58
61
  if (root === '[]') {
59
62
  obj = [];
60
63
  obj = obj.concat(internals.parseObject(chain, val, options));
61
64
  }
62
65
  else {
66
+ obj = options.plainObjects ? Object.create(null) : {};
63
67
  var cleanRoot = root[0] === '[' && root[root.length - 1] === ']' ? root.slice(1, root.length - 1) : root;
64
68
  var index = parseInt(cleanRoot, 10);
65
69
  var indexString = '' + index;
@@ -67,7 +71,8 @@ internals.parseObject = function (chain, val, options) {
67
71
  root !== cleanRoot &&
68
72
  indexString === cleanRoot &&
69
73
  index >= 0 &&
70
- index <= options.arrayLimit) {
74
+ (options.parseArrays &&
75
+ index <= options.arrayLimit)) {
71
76
 
72
77
  obj = [];
73
78
  obj[index] = internals.parseObject(chain, val, options);
@@ -87,6 +92,12 @@ internals.parseKeys = function (key, val, options) {
87
92
  return;
88
93
  }
89
94
 
95
+ // Transform dot notation to bracket notation
96
+
97
+ if (options.allowDots) {
98
+ key = key.replace(/\.([^\.\[]+)/g, '[$1]');
99
+ }
100
+
90
101
  // The regex chunks
91
102
 
92
103
  var parent = /^([^\[\]]*)/;
@@ -96,16 +107,20 @@ internals.parseKeys = function (key, val, options) {
96
107
 
97
108
  var segment = parent.exec(key);
98
109
 
99
- // Don't allow them to overwrite object prototype properties
100
-
101
- if (Object.prototype.hasOwnProperty(segment[1])) {
102
- return;
103
- }
104
-
105
110
  // Stash the parent if it exists
106
111
 
107
112
  var keys = [];
108
113
  if (segment[1]) {
114
+ // If we aren't using plain objects, optionally prefix keys
115
+ // that would overwrite object prototype properties
116
+ if (!options.plainObjects &&
117
+ Object.prototype.hasOwnProperty(segment[1])) {
118
+
119
+ if (!options.allowPrototypes) {
120
+ return;
121
+ }
122
+ }
123
+
109
124
  keys.push(segment[1]);
110
125
  }
111
126
 
@@ -115,9 +130,14 @@ internals.parseKeys = function (key, val, options) {
115
130
  while ((segment = child.exec(key)) !== null && i < options.depth) {
116
131
 
117
132
  ++i;
118
- if (!Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) {
119
- keys.push(segment[1]);
133
+ if (!options.plainObjects &&
134
+ Object.prototype.hasOwnProperty(segment[1].replace(/\[|\]/g, ''))) {
135
+
136
+ if (!options.allowPrototypes) {
137
+ continue;
138
+ }
120
139
  }
140
+ keys.push(segment[1]);
121
141
  }
122
142
 
123
143
  // If there's a remainder, just add whatever is left
@@ -132,21 +152,26 @@ internals.parseKeys = function (key, val, options) {
132
152
 
133
153
  module.exports = function (str, options) {
134
154
 
135
- if (str === '' ||
136
- str === null ||
137
- typeof str === 'undefined') {
138
-
139
- return {};
140
- }
141
-
142
155
  options = options || {};
143
156
  options.delimiter = typeof options.delimiter === 'string' || Utils.isRegExp(options.delimiter) ? options.delimiter : internals.delimiter;
144
157
  options.depth = typeof options.depth === 'number' ? options.depth : internals.depth;
145
158
  options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : internals.arrayLimit;
159
+ options.parseArrays = options.parseArrays !== false;
160
+ options.allowDots = options.allowDots !== false;
161
+ options.plainObjects = typeof options.plainObjects === 'boolean' ? options.plainObjects : internals.plainObjects;
162
+ options.allowPrototypes = typeof options.allowPrototypes === 'boolean' ? options.allowPrototypes : internals.allowPrototypes;
146
163
  options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : internals.parameterLimit;
164
+ options.strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : internals.strictNullHandling;
165
+
166
+ if (str === '' ||
167
+ str === null ||
168
+ typeof str === 'undefined') {
169
+
170
+ return options.plainObjects ? Object.create(null) : {};
171
+ }
147
172
 
148
173
  var tempObj = typeof str === 'string' ? internals.parseValues(str, options) : str;
149
- var obj = {};
174
+ var obj = options.plainObjects ? Object.create(null) : {};
150
175
 
151
176
  // Iterate over the keys and setup the new object
152
177
 
@@ -154,7 +179,7 @@ module.exports = function (str, options) {
154
179
  for (var i = 0, il = keys.length; i < il; ++i) {
155
180
  var key = keys[i];
156
181
  var newObj = internals.parseKeys(key, tempObj[key], options);
157
- obj = Utils.merge(obj, newObj);
182
+ obj = Utils.merge(obj, newObj, options);
158
183
  }
159
184
 
160
185
  return Utils.compact(obj);
package/lib/stringify.js CHANGED
@@ -9,27 +9,38 @@ var internals = {
9
9
  delimiter: '&',
10
10
  arrayPrefixGenerators: {
11
11
  brackets: function (prefix, key) {
12
+
12
13
  return prefix + '[]';
13
14
  },
14
15
  indices: function (prefix, key) {
16
+
15
17
  return prefix + '[' + key + ']';
16
18
  },
17
19
  repeat: function (prefix, key) {
20
+
18
21
  return prefix;
19
22
  }
20
- }
23
+ },
24
+ strictNullHandling: false
21
25
  };
22
26
 
23
27
 
24
- internals.stringify = function (obj, prefix, generateArrayPrefix) {
28
+ internals.stringify = function (obj, prefix, generateArrayPrefix, strictNullHandling, filter) {
25
29
 
26
- if (Utils.isBuffer(obj)) {
30
+ if (typeof filter === 'function') {
31
+ obj = filter(prefix, obj);
32
+ }
33
+ else if (Utils.isBuffer(obj)) {
27
34
  obj = obj.toString();
28
35
  }
29
36
  else if (obj instanceof Date) {
30
37
  obj = obj.toISOString();
31
38
  }
32
39
  else if (obj === null) {
40
+ if (strictNullHandling) {
41
+ return Utils.encode(prefix);
42
+ }
43
+
33
44
  obj = '';
34
45
  }
35
46
 
@@ -37,7 +48,7 @@ internals.stringify = function (obj, prefix, generateArrayPrefix) {
37
48
  typeof obj === 'number' ||
38
49
  typeof obj === 'boolean') {
39
50
 
40
- return [encodeURIComponent(prefix) + '=' + encodeURIComponent(obj)];
51
+ return [Utils.encode(prefix) + '=' + Utils.encode(obj)];
41
52
  }
42
53
 
43
54
  var values = [];
@@ -46,14 +57,15 @@ internals.stringify = function (obj, prefix, generateArrayPrefix) {
46
57
  return values;
47
58
  }
48
59
 
49
- var objKeys = Object.keys(obj);
60
+ var objKeys = Array.isArray(filter) ? filter : Object.keys(obj);
50
61
  for (var i = 0, il = objKeys.length; i < il; ++i) {
51
62
  var key = objKeys[i];
63
+
52
64
  if (Array.isArray(obj)) {
53
- values = values.concat(internals.stringify(obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix));
65
+ values = values.concat(internals.stringify(obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix, strictNullHandling, filter));
54
66
  }
55
67
  else {
56
- values = values.concat(internals.stringify(obj[key], prefix + '[' + key + ']', generateArrayPrefix));
68
+ values = values.concat(internals.stringify(obj[key], prefix + '[' + key + ']', generateArrayPrefix, strictNullHandling, filter));
57
69
  }
58
70
  }
59
71
 
@@ -65,6 +77,16 @@ module.exports = function (obj, options) {
65
77
 
66
78
  options = options || {};
67
79
  var delimiter = typeof options.delimiter === 'undefined' ? internals.delimiter : options.delimiter;
80
+ var strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : internals.strictNullHandling;
81
+ var objKeys;
82
+ var filter;
83
+ if (typeof options.filter === 'function') {
84
+ filter = options.filter;
85
+ obj = filter('', obj);
86
+ }
87
+ else if (Array.isArray(options.filter)) {
88
+ objKeys = filter = options.filter;
89
+ }
68
90
 
69
91
  var keys = [];
70
92
 
@@ -87,10 +109,12 @@ module.exports = function (obj, options) {
87
109
 
88
110
  var generateArrayPrefix = internals.arrayPrefixGenerators[arrayFormat];
89
111
 
90
- var objKeys = Object.keys(obj);
112
+ if (!objKeys) {
113
+ objKeys = Object.keys(obj);
114
+ }
91
115
  for (var i = 0, il = objKeys.length; i < il; ++i) {
92
116
  var key = objKeys[i];
93
- keys = keys.concat(internals.stringify(obj[key], key, generateArrayPrefix));
117
+ keys = keys.concat(internals.stringify(obj[key], key, generateArrayPrefix, strictNullHandling, filter));
94
118
  }
95
119
 
96
120
  return keys.join(delimiter);
package/lib/utils.js CHANGED
@@ -4,11 +4,15 @@
4
4
  // Declare internals
5
5
 
6
6
  var internals = {};
7
+ internals.hexTable = new Array(256);
8
+ for (var h = 0; h < 256; ++h) {
9
+ internals.hexTable[h] = '%' + ((h < 16 ? '0' : '') + h.toString(16)).toUpperCase();
10
+ }
7
11
 
8
12
 
9
- exports.arrayToObject = function (source) {
13
+ exports.arrayToObject = function (source, options) {
10
14
 
11
- var obj = {};
15
+ var obj = options.plainObjects ? Object.create(null) : {};
12
16
  for (var i = 0, il = source.length; i < il; ++i) {
13
17
  if (typeof source[i] !== 'undefined') {
14
18
 
@@ -20,7 +24,7 @@ exports.arrayToObject = function (source) {
20
24
  };
21
25
 
22
26
 
23
- exports.merge = function (target, source) {
27
+ exports.merge = function (target, source, options) {
24
28
 
25
29
  if (!source) {
26
30
  return target;
@@ -30,9 +34,12 @@ exports.merge = function (target, source) {
30
34
  if (Array.isArray(target)) {
31
35
  target.push(source);
32
36
  }
33
- else {
37
+ else if (typeof target === 'object') {
34
38
  target[source] = true;
35
39
  }
40
+ else {
41
+ target = [target, source];
42
+ }
36
43
 
37
44
  return target;
38
45
  }
@@ -45,7 +52,7 @@ exports.merge = function (target, source) {
45
52
  if (Array.isArray(target) &&
46
53
  !Array.isArray(source)) {
47
54
 
48
- target = exports.arrayToObject(target);
55
+ target = exports.arrayToObject(target, options);
49
56
  }
50
57
 
51
58
  var keys = Object.keys(source);
@@ -53,11 +60,11 @@ exports.merge = function (target, source) {
53
60
  var key = keys[k];
54
61
  var value = source[key];
55
62
 
56
- if (!target[key]) {
63
+ if (!Object.prototype.hasOwnProperty.call(target, key)) {
57
64
  target[key] = value;
58
65
  }
59
66
  else {
60
- target[key] = exports.merge(target[key], value);
67
+ target[key] = exports.merge(target[key], value, options);
61
68
  }
62
69
  }
63
70
 
@@ -74,6 +81,56 @@ exports.decode = function (str) {
74
81
  }
75
82
  };
76
83
 
84
+ exports.encode = function (str) {
85
+
86
+ // This code was originally written by Brian White (mscdex) for the io.js core querystring library.
87
+ // It has been adapted here for stricter adherence to RFC 3986
88
+ if (str.length === 0) {
89
+ return str;
90
+ }
91
+
92
+ if (typeof str !== 'string') {
93
+ str = '' + str;
94
+ }
95
+
96
+ var out = '';
97
+ for (var i = 0, il = str.length; i < il; ++i) {
98
+ var c = str.charCodeAt(i);
99
+
100
+ if (c === 0x2D || // -
101
+ c === 0x2E || // .
102
+ c === 0x5F || // _
103
+ c === 0x7E || // ~
104
+ (c >= 0x30 && c <= 0x39) || // 0-9
105
+ (c >= 0x41 && c <= 0x5A) || // a-z
106
+ (c >= 0x61 && c <= 0x7A)) { // A-Z
107
+
108
+ out += str[i];
109
+ continue;
110
+ }
111
+
112
+ if (c < 0x80) {
113
+ out += internals.hexTable[c];
114
+ continue;
115
+ }
116
+
117
+ if (c < 0x800) {
118
+ out += internals.hexTable[0xC0 | (c >> 6)] + internals.hexTable[0x80 | (c & 0x3F)];
119
+ continue;
120
+ }
121
+
122
+ if (c < 0xD800 || c >= 0xE000) {
123
+ out += internals.hexTable[0xE0 | (c >> 12)] + internals.hexTable[0x80 | ((c >> 6) & 0x3F)] + internals.hexTable[0x80 | (c & 0x3F)];
124
+ continue;
125
+ }
126
+
127
+ ++i;
128
+ c = 0x10000 + (((c & 0x3FF) << 10) | (str.charCodeAt(i) & 0x3FF));
129
+ out += internals.hexTable[0xF0 | (c >> 18)] + internals.hexTable[0x80 | ((c >> 12) & 0x3F)] + internals.hexTable[0x80 | ((c >> 6) & 0x3F)] + internals.hexTable[0x80 | (c & 0x3F)];
130
+ }
131
+
132
+ return out;
133
+ };
77
134
 
78
135
  exports.compact = function (obj, refs) {
79
136
 
@@ -114,6 +171,7 @@ exports.compact = function (obj, refs) {
114
171
 
115
172
 
116
173
  exports.isRegExp = function (obj) {
174
+
117
175
  return Object.prototype.toString.call(obj) === '[object RegExp]';
118
176
  };
119
177
 
@@ -127,6 +185,6 @@ exports.isBuffer = function (obj) {
127
185
  }
128
186
 
129
187
  return !!(obj.constructor &&
130
- obj.constructor.isBuffer &&
131
- obj.constructor.isBuffer(obj));
188
+ obj.constructor.isBuffer &&
189
+ obj.constructor.isBuffer(obj));
132
190
  };
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "qs",
3
- "version": "2.4.1",
3
+ "version": "4.0.0",
4
4
  "description": "A querystring parser that supports nesting and arrays, with a depth limit",
5
5
  "homepage": "https://github.com/hapijs/qs",
6
- "main": "index.js",
6
+ "main": "lib/index.js",
7
7
  "dependencies": {},
8
8
  "devDependencies": {
9
+ "browserify": "^10.2.1",
9
10
  "code": "1.x.x",
10
11
  "lab": "5.x.x"
11
12
  },
12
13
  "scripts": {
13
- "test": "make test-cov"
14
+ "test": "lab -a code -t 100 -L",
15
+ "test-cov-html": "lab -a code -r html -o coverage.html",
16
+ "dist": "browserify --standalone Qs lib/index.js > dist/qs.js"
14
17
  },
15
18
  "repository": {
16
19
  "type": "git",
@@ -20,10 +23,5 @@
20
23
  "querystring",
21
24
  "qs"
22
25
  ],
23
- "licenses": [
24
- {
25
- "type": "BSD",
26
- "url": "http://github.com/hapijs/qs/raw/master/LICENSE"
27
- }
28
- ]
26
+ "license": "BSD-3-Clause"
29
27
  }
package/test/parse.js CHANGED
@@ -28,11 +28,15 @@ describe('parse()', function () {
28
28
  expect(Qs.parse('a[>=]=23')).to.deep.equal({ a: { '>=': '23' } });
29
29
  expect(Qs.parse('a[<=>]==23')).to.deep.equal({ a: { '<=>': '=23' } });
30
30
  expect(Qs.parse('a[==]=23')).to.deep.equal({ a: { '==': '23' } });
31
- expect(Qs.parse('foo')).to.deep.equal({ foo: '' });
31
+ expect(Qs.parse('foo', { strictNullHandling: true })).to.deep.equal({ foo: null });
32
+ expect(Qs.parse('foo' )).to.deep.equal({ foo: '' });
33
+ expect(Qs.parse('foo=')).to.deep.equal({ foo: '' });
32
34
  expect(Qs.parse('foo=bar')).to.deep.equal({ foo: 'bar' });
33
35
  expect(Qs.parse(' foo = bar = baz ')).to.deep.equal({ ' foo ': ' bar = baz ' });
34
36
  expect(Qs.parse('foo=bar=baz')).to.deep.equal({ foo: 'bar=baz' });
35
37
  expect(Qs.parse('foo=bar&bar=baz')).to.deep.equal({ foo: 'bar', bar: 'baz' });
38
+ expect(Qs.parse('foo2=bar2&baz2=')).to.deep.equal({ foo2: 'bar2', baz2: '' });
39
+ expect(Qs.parse('foo=bar&baz', { strictNullHandling: true })).to.deep.equal({ foo: 'bar', baz: null });
36
40
  expect(Qs.parse('foo=bar&baz')).to.deep.equal({ foo: 'bar', baz: '' });
37
41
  expect(Qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World')).to.deep.equal({
38
42
  cht: 'p3',
@@ -43,6 +47,13 @@ describe('parse()', function () {
43
47
  done();
44
48
  });
45
49
 
50
+ it('allows disabling dot notation', function (done) {
51
+
52
+ expect(Qs.parse('a.b=c')).to.deep.equal({ a: { b: 'c' } });
53
+ expect(Qs.parse('a.b=c', { allowDots: false })).to.deep.equal({ 'a.b': 'c' });
54
+ done();
55
+ });
56
+
46
57
  it('parses a single nested string', function (done) {
47
58
 
48
59
  expect(Qs.parse('a[b]=c')).to.deep.equal({ a: { b: 'c' } });
@@ -156,7 +167,24 @@ describe('parse()', function () {
156
167
  expect(Qs.parse('foo[bad]=baz&foo[]=bar')).to.deep.equal({ foo: { bad: 'baz', '0': 'bar' } });
157
168
  expect(Qs.parse('foo[]=bar&foo[bad]=baz')).to.deep.equal({ foo: { '0': 'bar', bad: 'baz' } });
158
169
  expect(Qs.parse('foo[bad]=baz&foo[]=bar&foo[]=foo')).to.deep.equal({ foo: { bad: 'baz', '0': 'bar', '1': 'foo' } });
159
- expect(Qs.parse('foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb')).to.deep.equal({foo: [ {a: 'a', b: 'b'}, {a: 'aa', b: 'bb'} ]});
170
+ expect(Qs.parse('foo[0][a]=a&foo[0][b]=b&foo[1][a]=aa&foo[1][b]=bb')).to.deep.equal({ foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
171
+ expect(Qs.parse('a[]=b&a[t]=u&a[hasOwnProperty]=c')).to.deep.equal({ a: { '0': 'b', t: 'u', c: true } });
172
+ expect(Qs.parse('a[]=b&a[hasOwnProperty]=c&a[x]=y')).to.deep.equal({ a: { '0': 'b', '1': 'c', x: 'y' } });
173
+ done();
174
+ });
175
+
176
+ it('transforms arrays to objects (dot notation)', function (done) {
177
+
178
+ expect(Qs.parse('foo[0].baz=bar&fool.bad=baz')).to.deep.equal({ foo: [{ baz: 'bar' }], fool: { bad: 'baz' } });
179
+ expect(Qs.parse('foo[0].baz=bar&fool.bad.boo=baz')).to.deep.equal({ foo: [{ baz: 'bar' }], fool: { bad: { boo: 'baz' } } });
180
+ expect(Qs.parse('foo[0][0].baz=bar&fool.bad=baz')).to.deep.equal({ foo: [[{ baz: 'bar' }]], fool: { bad: 'baz' } });
181
+ expect(Qs.parse('foo[0].baz[0]=15&foo[0].bar=2')).to.deep.equal({ foo: [{ baz: ['15'], bar: '2' }] });
182
+ expect(Qs.parse('foo[0].baz[0]=15&foo[0].baz[1]=16&foo[0].bar=2')).to.deep.equal({ foo: [{ baz: ['15', '16'], bar: '2' }] });
183
+ expect(Qs.parse('foo.bad=baz&foo[0]=bar')).to.deep.equal({ foo: { bad: 'baz', '0': 'bar' } });
184
+ expect(Qs.parse('foo.bad=baz&foo[]=bar')).to.deep.equal({ foo: { bad: 'baz', '0': 'bar' } });
185
+ expect(Qs.parse('foo[]=bar&foo.bad=baz')).to.deep.equal({ foo: { '0': 'bar', bad: 'baz' } });
186
+ expect(Qs.parse('foo.bad=baz&foo[]=bar&foo[]=foo')).to.deep.equal({ foo: { bad: 'baz', '0': 'bar', '1': 'foo' } });
187
+ expect(Qs.parse('foo[0].a=a&foo[0].b=b&foo[1].a=aa&foo[1].b=bb')).to.deep.equal({ foo: [{ a: 'a', b: 'b' }, { a: 'aa', b: 'bb' }] });
160
188
  done();
161
189
  });
162
190
 
@@ -174,7 +202,8 @@ describe('parse()', function () {
174
202
 
175
203
  it('supports malformed uri characters', function (done) {
176
204
 
177
- expect(Qs.parse('{%:%}')).to.deep.equal({ '{%:%}': '' });
205
+ expect(Qs.parse('{%:%}', { strictNullHandling: true })).to.deep.equal({ '{%:%}': null });
206
+ expect(Qs.parse('{%:%}=')).to.deep.equal({ '{%:%}': '' });
178
207
  expect(Qs.parse('foo=%:%}')).to.deep.equal({ foo: '%:%}' });
179
208
  done();
180
209
  });
@@ -185,15 +214,6 @@ describe('parse()', function () {
185
214
  done();
186
215
  });
187
216
 
188
- it('cannot override prototypes', function (done) {
189
-
190
- var obj = Qs.parse('hasOwnProperty=bad&toString=bad&bad[toString]=bad&constructor=bad');
191
- expect(typeof obj.toString).to.equal('function');
192
- expect(typeof obj.bad.toString).to.equal('function');
193
- expect(typeof obj.constructor).to.equal('function');
194
- done();
195
- });
196
-
197
217
  it('cannot access Object prototype', function (done) {
198
218
 
199
219
  Qs.parse('constructor[prototype][bad]=bad');
@@ -212,7 +232,8 @@ describe('parse()', function () {
212
232
  it('allows for empty strings in arrays', function (done) {
213
233
 
214
234
  expect(Qs.parse('a[]=b&a[]=&a[]=c')).to.deep.equal({ a: ['b', '', 'c'] });
215
- expect(Qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]=')).to.deep.equal({ a: ['b', '', 'c', ''] });
235
+ expect(Qs.parse('a[0]=b&a[1]&a[2]=c&a[19]=', { strictNullHandling: true })).to.deep.equal({ a: ['b', null, 'c', ''] });
236
+ expect(Qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { strictNullHandling: true })).to.deep.equal({ a: ['b', '', 'c', null] });
216
237
  expect(Qs.parse('a[]=&a[]=b&a[]=c')).to.deep.equal({ a: ['', 'b', 'c'] });
217
238
  done();
218
239
  });
@@ -239,7 +260,8 @@ describe('parse()', function () {
239
260
 
240
261
  it('continues parsing when no parent is found', function (done) {
241
262
 
242
- expect(Qs.parse('[]&a=b')).to.deep.equal({ '0': '', a: 'b' });
263
+ expect(Qs.parse('[]=&a=b')).to.deep.equal({ '0': '', a: 'b' });
264
+ expect(Qs.parse('[]&a=b', { strictNullHandling: true })).to.deep.equal({ '0': null, a: 'b' });
243
265
  expect(Qs.parse('[foo]=bar')).to.deep.equal({ foo: 'bar' });
244
266
  done();
245
267
  });
@@ -310,16 +332,42 @@ describe('parse()', function () {
310
332
  done();
311
333
  });
312
334
 
335
+ it('allows disabling array parsing', function (done) {
336
+
337
+ expect(Qs.parse('a[0]=b&a[1]=c', { parseArrays: false })).to.deep.equal({ a: { '0': 'b', '1': 'c' } });
338
+ done();
339
+ });
340
+
313
341
  it('parses an object', function (done) {
314
342
 
315
343
  var input = {
316
- 'user[name]': {'pop[bob]': 3},
344
+ 'user[name]': { 'pop[bob]': 3 },
317
345
  'user[email]': null
318
346
  };
319
347
 
320
348
  var expected = {
321
349
  'user': {
322
- 'name': {'pop[bob]': 3},
350
+ 'name': { 'pop[bob]': 3 },
351
+ 'email': null
352
+ }
353
+ };
354
+
355
+ var result = Qs.parse(input);
356
+
357
+ expect(result).to.deep.equal(expected);
358
+ done();
359
+ });
360
+
361
+ it('parses an object in dot notation', function (done) {
362
+
363
+ var input = {
364
+ 'user.name': { 'pop[bob]': 3 },
365
+ 'user.email.': null
366
+ };
367
+
368
+ var expected = {
369
+ 'user': {
370
+ 'name': { 'pop[bob]': 3 },
323
371
  'email': null
324
372
  }
325
373
  };
@@ -333,13 +381,13 @@ describe('parse()', function () {
333
381
  it('parses an object and not child values', function (done) {
334
382
 
335
383
  var input = {
336
- 'user[name]': {'pop[bob]': { 'test': 3 }},
384
+ 'user[name]': { 'pop[bob]': { 'test': 3 } },
337
385
  'user[email]': null
338
386
  };
339
387
 
340
388
  var expected = {
341
389
  'user': {
342
- 'name': {'pop[bob]': { 'test': 3 }},
390
+ 'name': { 'pop[bob]': { 'test': 3 } },
343
391
  'email': null
344
392
  }
345
393
  };
@@ -360,12 +408,6 @@ describe('parse()', function () {
360
408
  done();
361
409
  });
362
410
 
363
- it('does not crash when using invalid dot notation', function (done) {
364
-
365
- expect(Qs.parse('roomInfoList[0].childrenAges[0]=15&roomInfoList[0].numberOfAdults=2')).to.deep.equal({ roomInfoList: [['15', '2']] });
366
- done();
367
- });
368
-
369
411
  it('does not crash when parsing circular references', function (done) {
370
412
 
371
413
  var a = {};
@@ -410,4 +452,27 @@ describe('parse()', function () {
410
452
  expect(Qs.parse({ a: re })).to.deep.equal({ a: re });
411
453
  done();
412
454
  });
455
+
456
+ it('can allow overwriting prototype properties', function (done) {
457
+
458
+ expect(Qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true })).to.deep.equal({ a: { hasOwnProperty: 'b' } }, { prototype: false });
459
+ expect(Qs.parse('hasOwnProperty=b', { allowPrototypes: true })).to.deep.equal({ hasOwnProperty: 'b' }, { prototype: false });
460
+ done();
461
+ });
462
+
463
+ it('can return plain objects', function (done) {
464
+
465
+ var expected = Object.create(null);
466
+ expected.a = Object.create(null);
467
+ expected.a.b = 'c';
468
+ expected.a.hasOwnProperty = 'd';
469
+ expect(Qs.parse('a[b]=c&a[hasOwnProperty]=d', { plainObjects: true })).to.deep.equal(expected);
470
+ expect(Qs.parse(null, { plainObjects: true })).to.deep.equal(Object.create(null));
471
+ var expectedArray = Object.create(null);
472
+ expectedArray.a = Object.create(null);
473
+ expectedArray.a['0'] = 'b';
474
+ expectedArray.a.c = 'd';
475
+ expect(Qs.parse('a[]=b&a[c]=d', { plainObjects: true })).to.deep.equal(expectedArray);
476
+ done();
477
+ });
413
478
  });
package/test/stringify.js CHANGED
@@ -26,6 +26,11 @@ describe('stringify()', function () {
26
26
  expect(Qs.stringify({ a: 'b' })).to.equal('a=b');
27
27
  expect(Qs.stringify({ a: 1 })).to.equal('a=1');
28
28
  expect(Qs.stringify({ a: 1, b: 2 })).to.equal('a=1&b=2');
29
+ expect(Qs.stringify({ a: 'A_Z' })).to.equal('a=A_Z');
30
+ expect(Qs.stringify({ a: '€' })).to.equal('a=%E2%82%AC');
31
+ expect(Qs.stringify({ a: '' })).to.equal('a=%EE%80%80');
32
+ expect(Qs.stringify({ a: 'א' })).to.equal('a=%D7%90');
33
+ expect(Qs.stringify({ a: '𐐷' })).to.equal('a=%F0%90%90%B7');
29
34
  done();
30
35
  });
31
36
 
@@ -106,9 +111,15 @@ describe('stringify()', function () {
106
111
  it('stringifies an empty value', function (done) {
107
112
 
108
113
  expect(Qs.stringify({ a: '' })).to.equal('a=');
114
+ expect(Qs.stringify({ a: null }, { strictNullHandling: true })).to.equal('a');
115
+
109
116
  expect(Qs.stringify({ a: '', b: '' })).to.equal('a=&b=');
110
- expect(Qs.stringify({ a: null })).to.equal('a=');
111
- expect(Qs.stringify({ a: { b: null } })).to.equal('a%5Bb%5D=');
117
+ expect(Qs.stringify({ a: null, b: '' }, { strictNullHandling: true })).to.equal('a&b=');
118
+
119
+ expect(Qs.stringify({ a: { b: '' } })).to.equal('a%5Bb%5D=');
120
+ expect(Qs.stringify({ a: { b: null } }, { strictNullHandling: true })).to.equal('a%5Bb%5D');
121
+ expect(Qs.stringify({ a: { b: null } }, { strictNullHandling: false })).to.equal('a%5Bb%5D=');
122
+
112
123
  done();
113
124
  });
114
125
 
@@ -143,7 +154,10 @@ describe('stringify()', function () {
143
154
  it('drops keys with a value of undefined', function (done) {
144
155
 
145
156
  expect(Qs.stringify({ a: undefined })).to.equal('');
146
- expect(Qs.stringify({ a: { b: undefined, c: null } })).to.equal('a%5Bc%5D=');
157
+
158
+ expect(Qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true })).to.equal('a%5Bc%5D');
159
+ expect(Qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false })).to.equal('a%5Bc%5D=');
160
+ expect(Qs.stringify({ a: { b: undefined, c: '' } })).to.equal('a%5Bc%5D=');
147
161
  done();
148
162
  });
149
163
 
@@ -163,14 +177,14 @@ describe('stringify()', function () {
163
177
 
164
178
  it('stringifies the weird object from qs', function (done) {
165
179
 
166
- expect(Qs.stringify({ 'my weird field': 'q1!2"\'w$5&7/z8)?' })).to.equal('my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F');
180
+ expect(Qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' })).to.equal('my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
167
181
  done();
168
182
  });
169
183
 
170
184
  it('skips properties that are part of the object prototype', function (done) {
171
185
 
172
186
  Object.prototype.crash = 'test';
173
- expect(Qs.stringify({ a: 'b'})).to.equal('a=b');
187
+ expect(Qs.stringify({ a: 'b' })).to.equal('a=b');
174
188
  expect(Qs.stringify({ a: { b: 'c' } })).to.equal('a%5Bb%5D=c');
175
189
  delete Object.prototype.crash;
176
190
  done();
@@ -206,4 +220,40 @@ describe('stringify()', function () {
206
220
  global.Buffer = tempBuffer;
207
221
  done();
208
222
  });
223
+
224
+ it('selects properties when filter=array', function (done) {
225
+
226
+ expect(Qs.stringify({ a: 'b' }, { filter: ['a'] })).to.equal('a=b');
227
+ expect(Qs.stringify({ a: 1 }, { filter: [] })).to.equal('');
228
+ expect(Qs.stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] })).to.equal('a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3');
229
+ done();
230
+
231
+ });
232
+
233
+ it('supports custom representations when filter=function', function (done) {
234
+
235
+ var calls = 0;
236
+ var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
237
+ var filterFunc = function (prefix, value) {
238
+
239
+ calls++;
240
+ if (calls === 1) {
241
+ expect(prefix).to.be.empty();
242
+ expect(value).to.equal(obj);
243
+ }
244
+ else if (prefix === 'c') {
245
+ return;
246
+ }
247
+ else if (value instanceof Date) {
248
+ expect(prefix).to.equal('e[f]');
249
+ return value.getTime();
250
+ }
251
+ return value;
252
+ };
253
+
254
+ expect(Qs.stringify(obj, { filter: filterFunc })).to.equal('a=b&e%5Bf%5D=1257894000000');
255
+ expect(calls).to.equal(5);
256
+ done();
257
+
258
+ });
209
259
  });
package/test/utils.js ADDED
@@ -0,0 +1,28 @@
1
+ // Load modules
2
+
3
+ var Code = require('code');
4
+ var Lab = require('lab');
5
+ var Utils = require('../lib/utils');
6
+
7
+
8
+ // Declare internals
9
+
10
+ var internals = {};
11
+
12
+
13
+ // Test shortcuts
14
+
15
+ var lab = exports.lab = Lab.script();
16
+ var expect = Code.expect;
17
+ var describe = lab.experiment;
18
+ var it = lab.test;
19
+
20
+
21
+ describe('merge()', function () {
22
+
23
+ it('can merge two objects with the same key', function (done) {
24
+
25
+ expect(Utils.merge({ a: 'b' }, { a: 'c' })).to.deep.equal({ a: ['b', 'c'] });
26
+ done();
27
+ });
28
+ });
package/.jshintignore DELETED
@@ -1 +0,0 @@
1
- node_modules
package/.jshintrc DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "node": true,
3
-
4
- "curly": true,
5
- "latedef": true,
6
- "quotmark": true,
7
- "undef": true,
8
- "unused": true,
9
- "trailing": true
10
- }
package/Makefile DELETED
@@ -1,8 +0,0 @@
1
- test:
2
- @node node_modules/lab/bin/lab -a code -L
3
- test-cov:
4
- @node node_modules/lab/bin/lab -a code -t 100 -L
5
- test-cov-html:
6
- @node node_modules/lab/bin/lab -a code -L -r html -o coverage.html
7
-
8
- .PHONY: test test-cov test-cov-html
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./lib/');