UNPKG

qs

Version:

A querystring parser that supports nesting and arrays, with a depth limit

513 lines (448 loc) • 17.8 kB
'use strict'; var formats = require('./formats'); var getSideChannel = require('side-channel'); var defineProperty = require('es-define-property'); var has = Object.prototype.hasOwnProperty; var isArray = Array.isArray; // Track objects created from arrayLimit overflow using side-channel // Stores the current max numeric index for O(1) lookup var overflowChannel = getSideChannel(); var markOverflow = function markOverflow(obj, maxIndex) { overflowChannel.set(obj, maxIndex); return obj; }; var isOverflow = function isOverflow(obj) { return overflowChannel.has(obj); }; var getMaxIndex = function getMaxIndex(obj) { return overflowChannel.get(obj); }; var setMaxIndex = function setMaxIndex(obj, maxIndex) { overflowChannel.set(obj, maxIndex); }; // Track containers produced by a `strictMerge` conflict wrap, and their current max numeric index, // so repeated conflicts on one path append instead of nesting a new wrapper each time var conflictChannel = getSideChannel(); var markConflict = function markConflict(obj, maxIndex) { conflictChannel.set(obj, maxIndex); return obj; }; var isConflict = function isConflict(obj) { return conflictChannel.has(obj); }; var getConflictIndex = function getConflictIndex(obj) { return conflictChannel.get(obj); }; var hexTable = (function () { var array = []; for (var i = 0; i < 256; ++i) { array[array.length] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); } return array; }()); var compactQueue = function compactQueue(queue) { while (queue.length > 1) { var item = queue.pop(); var obj = item.obj[item.prop]; if (isArray(obj)) { var compacted = []; for (var j = 0; j < obj.length; ++j) { if (typeof obj[j] !== 'undefined') { compacted[compacted.length] = obj[j]; } } item.obj[item.prop] = compacted; } } }; var arrayToObject = function arrayToObject(source, options) { var obj = options && options.plainObjects ? { __proto__: null } : {}; for (var i = 0; i < source.length; ++i) { if (typeof source[i] !== 'undefined') { obj[i] = source[i]; } } return obj; }; var setProperty = function setProperty(obj, key, value) { if (key === '__proto__' && defineProperty) { defineProperty(obj, key, { configurable: true, enumerable: true, value: value, writable: true }); } else { obj[key] = value; } }; var maxNumericIndex = function maxNumericIndex(obj, min) { var max = min; var keys = Object.keys(obj); for (var i = 0; i < keys.length; ++i) { var num = parseInt(keys[i], 10); if (String(num) === keys[i] && num > max) { max = num; } } return max; }; var limitedPair = function limitedPair(first, second, options) { var pair = [first, second]; if (options && typeof options.arrayLimit === 'number' && pair.length > options.arrayLimit) { if (options.throwOnLimitExceeded) { throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } return markOverflow(arrayToObject(pair, options), 1); } return pair; }; var merge = function merge(target, source, options) { /* eslint no-param-reassign: 0 */ if (!source) { return target; } if (typeof source !== 'object' && typeof source !== 'function') { if (isArray(target)) { var nextIndex = target.length; if (options && typeof options.arrayLimit === 'number' && nextIndex >= options.arrayLimit) { if (options.throwOnLimitExceeded) { throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } return markOverflow(arrayToObject(target.concat(source), options), nextIndex); } target[nextIndex] = source; } else if (target && typeof target === 'object') { if (isOverflow(target)) { // Add at next numeric index for overflow objects var newIndex = getMaxIndex(target) + 1; target[newIndex] = source; setMaxIndex(target, newIndex); } else if (options && options.strictMerge && isConflict(target)) { var nextConflict = getConflictIndex(target) + 1; if ( options.throwOnLimitExceeded && typeof options.arrayLimit === 'number' && nextConflict >= options.arrayLimit ) { throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } target[nextConflict] = source; markConflict(target, nextConflict); } else if (options && options.strictMerge) { var conflict = limitedPair(target, source, options); return isArray(conflict) ? markConflict(conflict, 1) : conflict; } else if ( (options && (options.plainObjects || options.allowPrototypes)) || !has.call(Object.prototype, source) ) { target[source] = true; } } else { return limitedPair(target, source, options); } return target; } if (!target || typeof target !== 'object') { if (isOverflow(source)) { // Create new object with target at 0, source values shifted by 1 var sourceKeys = Object.keys(source); var result = options && options.plainObjects ? { __proto__: null, 0: target } : { 0: target }; for (var m = 0; m < sourceKeys.length; m++) { var oldKey = parseInt(sourceKeys[m], 10); result[oldKey + 1] = source[sourceKeys[m]]; } return markOverflow(result, getMaxIndex(source) + 1); } var combined = [target].concat(source); if (options && typeof options.arrayLimit === 'number' && combined.length > options.arrayLimit) { if (options.throwOnLimitExceeded) { throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } return markOverflow(arrayToObject(combined, options), combined.length - 1); } return combined; } var mergeTarget = target; if (isArray(target) && !isArray(source)) { mergeTarget = arrayToObject(target, options); if (isConflict(target)) { // the array may have grown past its last conflict, so continue after its last index markConflict(mergeTarget, target.length - 1); } } if (isArray(target) && isArray(source)) { source.forEach(function (item, i) { if (has.call(target, i)) { var targetItem = target[i]; if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { target[i] = merge(targetItem, item, options); } else { target[target.length] = item; } } else { target[i] = item; } }); if (options && typeof options.arrayLimit === 'number' && target.length > options.arrayLimit) { if (options.throwOnLimitExceeded) { throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (options.arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } return markOverflow(arrayToObject(target, options), target.length - 1); } return target; } return Object.keys(source).reduce(function (acc, key) { var value = source[key]; if (has.call(acc, key)) { setProperty(acc, key, merge(acc[key], value, options)); } else { setProperty(acc, key, value); } if (isOverflow(source) && !isOverflow(acc)) { markOverflow(acc, maxNumericIndex(acc, getMaxIndex(source))); } if (isOverflow(acc)) { var keyNum = parseInt(key, 10); if (String(keyNum) === key && keyNum >= 0 && keyNum > getMaxIndex(acc)) { setMaxIndex(acc, keyNum); } } if (isConflict(acc)) { var conflictKey = parseInt(key, 10); if (String(conflictKey) === key && conflictKey > getConflictIndex(acc)) { markConflict(acc, conflictKey); } } return acc; }, mergeTarget); }; var assign = function assignSingleSource(target, source) { return Object.keys(source).reduce(function (acc, key) { setProperty(acc, key, source[key]); return acc; }, target); }; var decode = function (str, defaultDecoder, charset) { // With no `%` there is no escape to expand, in either charset: `unescape` has // nothing to match, and `decodeURIComponent` is the identity on such a string // (and cannot throw). So only `+` -> ' ' remains, and often not even that. if (typeof str === 'string' && str.indexOf('%') === -1) { return str.indexOf('+') === -1 ? str : str.replace(/\+/g, ' '); } 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(strWithoutPlus); } catch (e) { return strWithoutPlus; } }; var hasToPrimitive = typeof Symbol === 'function' && typeof Symbol.toPrimitive === 'symbol'; var arrayJoin = Array.prototype.join; var arrayToString = Array.prototype.toString; var objectToString = Object.prototype.toString; // an object with no callable toString (eg, from JSON, or null-prototype) would make String() or join() throw, // so it becomes its primitive valueOf or else Object.prototype.toString; arrays are walked like join walks them, // copied only once an element changes, with an array already being joined becoming '' as engines do for cycles var toCoercible = function toCoercible(value, joining) { if (!value || (typeof value !== 'object' && typeof value !== 'function')) { return value; } if (hasToPrimitive && value[Symbol.toPrimitive] != null) { return value; } if (isArray(value) && value.toString === arrayToString && value.join === arrayJoin) { var ancestors = joining || getSideChannel(); if (ancestors.has(value)) { return ''; } ancestors.set(value, true); var copy; for (var i = 0; i < value.length; i += 1) { var item = value[i]; var coerced = toCoercible(item, ancestors); if (!copy && coerced !== item) { copy = []; for (var j = 0; j < i; j += 1) { copy[j] = value[j]; } } if (copy) { copy[i] = coerced; } } ancestors['delete'](value); return copy || value; } if (typeof value.toString === 'function') { return value; } var valueOf = value.valueOf; if (typeof valueOf === 'function') { var primitive = valueOf.call(value); if (!primitive || (typeof primitive !== 'object' && typeof primitive !== 'function')) { return String(primitive); } } return objectToString.call(value); }; var limit = 1024; /* eslint operator-linebreak: [2, "before"] */ var encode = function encode(str, defaultEncoder, charset, kind, format) { // 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) { return str; } var string = str; if (typeof str === 'symbol') { string = Symbol.prototype.toString.call(str); } else if (typeof str !== 'string') { string = String(toCoercible(str)); } if (charset === 'iso-8859-1') { // `escape` leaves `+` untouched, but a literal `+` is decoded back to a space, // so it must be percent-encoded to round-trip (the utf-8 path already does this). return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; }).replace(/\+/g, '%2B'); } var out = ''; for (var j = 0; j < string.length; j += limit) { var segment = string.length >= limit ? string.slice(j, j + limit) : string; if (j + limit < string.length) { var last = segment.charCodeAt(segment.length - 1); if (last >= 0xD800 && last <= 0xDBFF) { segment = segment.slice(0, -1); j -= 1; } } var arr = []; for (var i = 0; i < segment.length; ++i) { var c = segment.charCodeAt(i); if ( c === 0x2D // - || c === 0x2E // . || c === 0x5F // _ || c === 0x7E // ~ || (c >= 0x30 && c <= 0x39) // 0-9 || (c >= 0x41 && c <= 0x5A) // a-z || (c >= 0x61 && c <= 0x7A) // A-Z || (format === formats.RFC1738 && (c === 0x28 || c === 0x29)) // ( ) ) { arr[arr.length] = segment.charAt(i); continue; } if (c < 0x80) { arr[arr.length] = hexTable[c]; continue; } if (c < 0x800) { arr[arr.length] = hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]; continue; } if (c < 0xD800 || c >= 0xE000) { arr[arr.length] = hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]; continue; } i += 1; c = 0x10000 + (((c & 0x3FF) << 10) | (segment.charCodeAt(i) & 0x3FF)); arr[arr.length] = hexTable[0xF0 | (c >> 18)] + hexTable[0x80 | ((c >> 12) & 0x3F)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]; } out += arr.join(''); } return out; }; var compact = function compact(value) { var queue = [{ obj: { o: value }, prop: 'o' }]; var refs = getSideChannel(); for (var i = 0; i < queue.length; ++i) { var item = queue[i]; var obj = item.obj[item.prop]; var keys = Object.keys(obj); for (var j = 0; j < keys.length; ++j) { var key = keys[j]; var val = obj[key]; if (typeof val === 'object' && val !== null && !refs.has(val)) { queue[queue.length] = { obj: obj, prop: key }; refs.set(val, true); } } } compactQueue(queue); return value; }; var isRegExp = function isRegExp(obj) { return Object.prototype.toString.call(obj) === '[object RegExp]'; }; var isBuffer = function isBuffer(obj) { if (!obj || typeof obj !== 'object') { return false; } return !!(obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)); }; var combine = function combine(a, b, arrayLimit, plainObjects, throwOnLimitExceeded) { // If 'a' is already an overflow object, add to it if (isOverflow(a)) { if (throwOnLimitExceeded) { throw new RangeError('Array limit exceeded. Only ' + arrayLimit + ' element' + (arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } // spread `b` one level, matching the `[].concat(a, b)` used below, so a // collection appended to an already-overflowed object is flattened // rather than nested under a single index var bValues = isArray(b) ? b : [b]; var newIndex = getMaxIndex(a); for (var i = 0; i < bValues.length; ++i) { newIndex += 1; a[newIndex] = bValues[i]; } setMaxIndex(a, newIndex); return a; } var result = [].concat(a, b); if (result.length > arrayLimit) { if (throwOnLimitExceeded) { throw new RangeError('Array limit exceeded. Only ' + arrayLimit + ' element' + (arrayLimit === 1 ? '' : 's') + ' allowed in an array.'); } return markOverflow(arrayToObject(result, { plainObjects: plainObjects }), result.length - 1); } return result; }; var maybeMap = function maybeMap(val, fn) { if (isArray(val)) { var mapped = []; for (var i = 0; i < val.length; i += 1) { mapped[mapped.length] = fn(val[i]); } return mapped; } return fn(val); }; module.exports = { arrayToObject: arrayToObject, assign: assign, combine: combine, compact: compact, decode: decode, encode: encode, isBuffer: isBuffer, isOverflow: isOverflow, isRegExp: isRegExp, markOverflow: markOverflow, maybeMap: maybeMap, merge: merge, toCoercible: toCoercible };