From 34af57edde61639054ea7b38fdfce050cffdab29 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 17 Sep 2018 16:06:12 -0700 Subject: [PATCH] v6.6.0 --- CHANGELOG.md | 16 +++++ dist/qs.js | 173 ++++++++++++++++++++++++++++++++++++++++----------- package.json | 2 +- 3 files changed, 153 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe523209..13e2c77c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## **6.6.0** +- [New] Add support for iso-8859-1, utf8 "sentinel" and numeric entities (#268) +- [New] move two-value combine to a `utils` function (#189) +- [Fix] `stringify`: fix a crash with `strictNullHandling` and a custom `filter`/`serializeDate` (#279) +- [Fix] when `parseArrays` is false, properly handle keys ending in `[]` (#260) +- [Fix] `stringify`: do not crash in an obscure combo of `interpretNumericEntities`, a bad custom `decoder`, & `iso-8859-1` +- [Fix] `utils`: `merge`: fix crash when `source` is a truthy primitive & no options are provided +- [refactor] `stringify`: Avoid arr = arr.concat(...), push to the existing instance (#269) +- [Refactor] `parse`: only need to reassign the var once +- [Refactor] `parse`/`stringify`: clean up `charset` options checking; fix defaults +- [Refactor] add missing defaults +- [Refactor] `parse`: one less `concat` call +- [Refactor] `utils`: `compactQueue`: make it explicitly side-effecting +- [Dev Deps] update `browserify, `eslint`, `@ljharb/eslint-config`, `iconv-lite`, `safe-publish-latest`, `tape` +- [Tests] up to `node` `v10.10`, `v9.11`, `v8.12`, `v6.14`, `v4.9`; pin included builds to LTS + ## **6.5.2** - [Fix] use `safer-buffer` instead of `Buffer` constructor - [Refactor] utils: `module.exports` one thing, instead of mutating `exports` (#230) diff --git a/dist/qs.js b/dist/qs.js index ecf7ba44..b9482991 100644 --- a/dist/qs.js +++ b/dist/qs.js @@ -42,21 +42,62 @@ var defaults = { allowDots: false, allowPrototypes: false, arrayLimit: 20, + charset: 'utf-8', + charsetSentinel: false, decoder: utils.decode, delimiter: '&', depth: 5, + ignoreQueryPrefix: false, + interpretNumericEntities: false, parameterLimit: 1000, + parseArrays: true, plainObjects: false, strictNullHandling: false }; +var interpretNumericEntities = function (str) { + return str.replace(/&#(\d+);/g, function ($0, numberStr) { + return String.fromCharCode(parseInt(numberStr, 10)); + }); +}; + +// This is what browsers will submit when the ✓ character occurs in an +// application/x-www-form-urlencoded body and the encoding of the page containing +// the form is iso-8859-1, or when the submitted form has an accept-charset +// attribute of iso-8859-1. Presumably also with other charsets that do not contain +// the ✓ character, such as us-ascii. +var isoSentinel = 'utf8=%26%2310003%3B'; // encodeURIComponent('✓') + +// These are the percent-encoded utf-8 octets representing a checkmark, indicating that the request actually is utf-8 encoded. +var charsetSentinel = 'utf8=%E2%9C%93'; // encodeURIComponent('✓') + var parseValues = function parseQueryStringValues(str, options) { var obj = {}; var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str; var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit; var parts = cleanStr.split(options.delimiter, limit); + var skipIndex = -1; // Keep track of where the utf8 sentinel was found + var i; + + var charset = options.charset; + if (options.charsetSentinel) { + for (i = 0; i < parts.length; ++i) { + if (parts[i].indexOf('utf8=') === 0) { + if (parts[i] === charsetSentinel) { + charset = 'utf-8'; + } else if (parts[i] === isoSentinel) { + charset = 'iso-8859-1'; + } + skipIndex = i; + i = parts.length; // The eslint settings do not allow break; + } + } + } - for (var i = 0; i < parts.length; ++i) { + for (i = 0; i < parts.length; ++i) { + if (i === skipIndex) { + continue; + } var part = parts[i]; var bracketEqualsPos = part.indexOf(']='); @@ -64,14 +105,18 @@ var parseValues = function parseQueryStringValues(str, options) { var key, val; if (pos === -1) { - key = options.decoder(part, defaults.decoder); + key = options.decoder(part, defaults.decoder, charset); val = options.strictNullHandling ? null : ''; } else { - key = options.decoder(part.slice(0, pos), defaults.decoder); - val = options.decoder(part.slice(pos + 1), defaults.decoder); + key = options.decoder(part.slice(0, pos), defaults.decoder, charset); + val = options.decoder(part.slice(pos + 1), defaults.decoder, charset); + } + + if (val && options.interpretNumericEntities && charset === 'iso-8859-1') { + val = interpretNumericEntities(val); } if (has.call(obj, key)) { - obj[key] = [].concat(obj[key]).concat(val); + obj[key] = utils.combine(obj[key], val); } else { obj[key] = val; } @@ -87,14 +132,15 @@ var parseObject = function (chain, val, options) { var obj; var root = chain[i]; - if (root === '[]') { - obj = []; - obj = obj.concat(leaf); + if (root === '[]' && options.parseArrays) { + obj = [].concat(leaf); } else { obj = options.plainObjects ? Object.create(null) : {}; var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root; var index = parseInt(cleanRoot, 10); - if ( + if (!options.parseArrays && cleanRoot === '') { + obj = { 0: leaf }; + } else if ( !isNaN(index) && root !== cleanRoot && String(index) === cleanRoot @@ -136,8 +182,7 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options) { var keys = []; if (parent) { - // If we aren't using plain objects, optionally prefix keys - // that would overwrite object prototype properties + // If we aren't using plain objects, optionally prefix keys that would overwrite object prototype properties if (!options.plainObjects && has.call(Object.prototype, parent)) { if (!options.allowPrototypes) { return; @@ -182,12 +227,19 @@ module.exports = function (str, opts) { options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : defaults.arrayLimit; options.parseArrays = options.parseArrays !== false; options.decoder = typeof options.decoder === 'function' ? options.decoder : defaults.decoder; - options.allowDots = typeof options.allowDots === 'boolean' ? options.allowDots : defaults.allowDots; + options.allowDots = typeof options.allowDots === 'undefined' ? defaults.allowDots : !!options.allowDots; options.plainObjects = typeof options.plainObjects === 'boolean' ? options.plainObjects : defaults.plainObjects; options.allowPrototypes = typeof options.allowPrototypes === 'boolean' ? options.allowPrototypes : defaults.allowPrototypes; options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : defaults.parameterLimit; options.strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : defaults.strictNullHandling; + if (typeof options.charset !== 'undefined' && options.charset !== 'utf-8' && options.charset !== 'iso-8859-1') { + throw new Error('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + if (typeof options.charset === 'undefined') { + options.charset = defaults.charset; + } + if (str === '' || str === null || typeof str === 'undefined') { return options.plainObjects ? Object.create(null) : {}; } @@ -225,13 +277,25 @@ var arrayPrefixGenerators = { } }; +var isArray = Array.isArray; +var push = Array.prototype.push; +var pushToArray = function (arr, valueOrArray) { + push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]); +}; + var toISO = Date.prototype.toISOString; var defaults = { + addQueryPrefix: false, + allowDots: false, + charset: 'utf-8', + charsetSentinel: false, delimiter: '&', encode: true, encoder: utils.encode, encodeValuesOnly: false, + // deprecated + indices: false, serializeDate: function serializeDate(date) { // eslint-disable-line func-name-matching return toISO.call(date); }, @@ -251,16 +315,19 @@ var stringify = function stringify( // eslint-disable-line func-name-matching allowDots, serializeDate, formatter, - encodeValuesOnly + encodeValuesOnly, + charset ) { var obj = object; if (typeof filter === 'function') { obj = filter(prefix, obj); } else if (obj instanceof Date) { obj = serializeDate(obj); - } else if (obj === null) { + } + + if (obj === null) { if (strictNullHandling) { - return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder) : prefix; + return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset) : prefix; } obj = ''; @@ -268,8 +335,8 @@ var stringify = function stringify( // eslint-disable-line func-name-matching if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || utils.isBuffer(obj)) { if (encoder) { - var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder); - return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder))]; + var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder, charset); + return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder, charset))]; } return [formatter(prefix) + '=' + formatter(String(obj))]; } @@ -296,7 +363,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching } if (Array.isArray(obj)) { - values = values.concat(stringify( + pushToArray(values, stringify( obj[key], generateArrayPrefix(prefix, key), generateArrayPrefix, @@ -308,10 +375,11 @@ var stringify = function stringify( // eslint-disable-line func-name-matching allowDots, serializeDate, formatter, - encodeValuesOnly + encodeValuesOnly, + charset )); } else { - values = values.concat(stringify( + pushToArray(values, stringify( obj[key], prefix + (allowDots ? '.' + key : '[' + key + ']'), generateArrayPrefix, @@ -323,7 +391,8 @@ var stringify = function stringify( // eslint-disable-line func-name-matching allowDots, serializeDate, formatter, - encodeValuesOnly + encodeValuesOnly, + charset )); } } @@ -345,9 +414,14 @@ module.exports = function (object, opts) { var encode = typeof options.encode === 'boolean' ? options.encode : defaults.encode; var encoder = typeof options.encoder === 'function' ? options.encoder : defaults.encoder; var sort = typeof options.sort === 'function' ? options.sort : null; - var allowDots = typeof options.allowDots === 'undefined' ? false : options.allowDots; + var allowDots = typeof options.allowDots === 'undefined' ? defaults.allowDots : !!options.allowDots; var serializeDate = typeof options.serializeDate === 'function' ? options.serializeDate : defaults.serializeDate; var encodeValuesOnly = typeof options.encodeValuesOnly === 'boolean' ? options.encodeValuesOnly : defaults.encodeValuesOnly; + var charset = options.charset || defaults.charset; + if (typeof options.charset !== 'undefined' && options.charset !== 'utf-8' && options.charset !== 'iso-8859-1') { + throw new Error('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + if (typeof options.format === 'undefined') { options.format = formats['default']; } else if (!Object.prototype.hasOwnProperty.call(formats.formatters, options.format)) { @@ -396,8 +470,7 @@ module.exports = function (object, opts) { if (skipNulls && obj[key] === null) { continue; } - - keys = keys.concat(stringify( + pushToArray(keys, stringify( obj[key], key, generateArrayPrefix, @@ -409,13 +482,24 @@ module.exports = function (object, opts) { allowDots, serializeDate, formatter, - encodeValuesOnly + encodeValuesOnly, + charset )); } var joined = keys.join(delimiter); var prefix = options.addQueryPrefix === true ? '?' : ''; + if (options.charsetSentinel) { + if (charset === 'iso-8859-1') { + // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark + prefix += 'utf8=%26%2310003%3B&'; + } else { + // encodeURIComponent('✓') + prefix += 'utf8=%E2%9C%93&'; + } + } + return joined.length > 0 ? prefix + joined : ''; }; @@ -434,11 +518,9 @@ var hexTable = (function () { }()); var compactQueue = function compactQueue(queue) { - var obj; - - while (queue.length) { + while (queue.length > 1) { var item = queue.pop(); - obj = item.obj[item.prop]; + var obj = item.obj[item.prop]; if (Array.isArray(obj)) { var compacted = []; @@ -452,8 +534,6 @@ var compactQueue = function compactQueue(queue) { item.obj[item.prop] = compacted; } } - - return obj; }; var arrayToObject = function arrayToObject(source, options) { @@ -476,7 +556,7 @@ var merge = function merge(target, source, options) { if (Array.isArray(target)) { target.push(source); } else if (typeof target === 'object') { - if (options.plainObjects || options.allowPrototypes || !has.call(Object.prototype, source)) { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source)) { target[source] = true; } } else { @@ -529,15 +609,21 @@ var assign = function assignSingleSource(target, source) { }, target); }; -var decode = function (str) { +var decode = function (str, decoder, charset) { + var strWithoutPlus = str.replace(/\+/g, ' '); + if (charset === 'iso-8859-1') { + // unescape never throws, no try...catch needed: + return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); + } + // utf-8 try { - return decodeURIComponent(str.replace(/\+/g, ' ')); + return decodeURIComponent(strWithoutPlus); } catch (e) { - return str; + return strWithoutPlus; } }; -var encode = function encode(str) { +var encode = function encode(str, defaultEncoder, charset) { // This code was originally written by Brian White (mscdex) for the io.js core querystring library. // It has been adapted here for stricter adherence to RFC 3986 if (str.length === 0) { @@ -546,6 +632,12 @@ var encode = function encode(str) { var string = typeof str === 'string' ? str : String(str); + if (charset === 'iso-8859-1') { + return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { + return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; + }); + } + var out = ''; for (var i = 0; i < string.length; ++i) { var c = string.charCodeAt(i); @@ -608,7 +700,9 @@ var compact = function compact(value) { } } - return compactQueue(queue); + compactQueue(queue); + + return value; }; var isRegExp = function isRegExp(obj) { @@ -623,9 +717,14 @@ var isBuffer = function isBuffer(obj) { return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); }; +var combine = function combine(a, b) { + return [].concat(a, b); +}; + module.exports = { arrayToObject: arrayToObject, assign: assign, + combine: combine, compact: compact, decode: decode, encode: encode, diff --git a/package.json b/package.json index 27440aae..ced1effc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "qs", "description": "A querystring parser that supports nesting and arrays, with a depth limit", "homepage": "https://github.com/ljharb/qs", - "version": "6.5.2", + "version": "6.6.0", "repository": { "type": "git", "url": "https://github.com/ljharb/qs.git"