var _ = require('../util').lodash, Property = require('./property').Property, PropertyList = require('./property-list').PropertyList, E = '', AMPERSAND = '&', STRING = 'string', EQUALS = '=', EMPTY = '', HASH = '#', REGEX_HASH = /#/g, REGEX_EQUALS = /=/g, // eslint-disable-line no-div-regex REGEX_AMPERSAND = /&/g, REGEX_EXTRACT_VARS = /{{[^{}]*[&#=][^{}]*}}/g, QueryParam, /** * Percent encode reserved chars (&, = and #) in the given string. * * @private * @param {String} str - * @param {Boolean} encodeEquals - * @returns {String} */ encodeReservedChars = function (str, encodeEquals) { if (!str) { return str; } // eslint-disable-next-line lodash/prefer-includes str.indexOf(AMPERSAND) !== -1 && (str = str.replace(REGEX_AMPERSAND, '%26')); // eslint-disable-next-line lodash/prefer-includes str.indexOf(HASH) !== -1 && (str = str.replace(REGEX_HASH, '%23')); // eslint-disable-next-line lodash/prefer-includes encodeEquals && str.indexOf(EQUALS) !== -1 && (str = str.replace(REGEX_EQUALS, '%3D')); return str; }, /** * Normalize the given param string by percent-encoding the reserved chars * such that it won't affect the re-parsing. * * @note `&`, `=` and `#` needs to be percent-encoded otherwise re-parsing * the same URL string will generate different output * * @private * @param {String} str - * @param {Boolean} encodeEquals - * @returns {String} */ normalizeParam = function (str, encodeEquals) { // bail out if the given sting is null or empty if (!(str && typeof str === STRING)) { return str; } // bail out if the given string does not include reserved chars // eslint-disable-next-line lodash/prefer-includes if (str.indexOf(AMPERSAND) === -1 && str.indexOf(HASH) === -1) { // eslint-disable-next-line lodash/prefer-includes if (!(encodeEquals && str.indexOf(EQUALS) !== -1)) { return str; } } var normalizedString = '', pointer = 0, variable, match, index; // find all the instances of {{}} which includes reserved chars while ((match = REGEX_EXTRACT_VARS.exec(str)) !== null) { variable = match[0]; index = match.index; // [pointer, index) string is normalized + the matched variable normalizedString += encodeReservedChars(str.slice(pointer, index), encodeEquals) + variable; // update the pointer pointer = index + variable.length; } // whatever left in the string is normalized as well if (pointer < str.length) { normalizedString += encodeReservedChars(str.slice(pointer), encodeEquals); } return normalizedString; }; /** * @typedef QueryParam.definition * @property {String} key The name ("key") of the query parameter. * @property {String} value The value of the parameter. */ _.inherit(( /** * Represents a URL query parameter, which can exist in request URL or POST data. * * @constructor * @extends {Property} * @param {FormParam.definition|String} options Pass the initial definition of the query parameter. In case of * string, the query parameter is parsed using {@link QueryParam.parseSingle}. */ QueryParam = function PostmanQueryParam (options) { // this constructor is intended to inherit and as such the super constructor is required to be executed QueryParam.super_.apply(this, arguments); this.update(options); }), Property); _.assign(QueryParam.prototype, /** @lends QueryParam.prototype */ { /** * Converts the QueryParameter to a single param string. * * @returns {String} */ toString () { return QueryParam.unparseSingle(this); }, /** * Updates the key and value of the query parameter * * @param {String|Object} param - * @param {String} param.key - * @param {String=} [param.value] - */ update (param) { _.assign(this, /** @lends QueryParam.prototype */ _.isString(param) ? QueryParam.parseSingle(param) : { key: _.get(param, 'key'), // we do not replace falsey with blank string since null has a meaning value: _.get(param, 'value') }); _.has(param, 'system') && (this.system = param.system); }, valueOf () { return _.isString(this.value) ? this.value : EMPTY; } }); _.assign(QueryParam, /** @lends QueryParam */ { /** * Defines the name of this property for internal use. * * @private * @readOnly * @type {String} */ _postman_propertyName: 'QueryParam', /** * Declare the list index key, so that property lists of query parameters work correctly * * @type {String} */ _postman_propertyIndexKey: 'key', /** * Query params can have multiple values, so set this to true. * * @type {Boolean} */ _postman_propertyAllowsMultipleValues: true, /** * Parse a query string into an array of objects, where each object contains a key and a value. * * @param {String} query - * @returns {Array} */ parse: function (query) { return _.isString(query) ? query.split(AMPERSAND).map(QueryParam.parseSingle) : []; }, /** * Parses a single query parameter. * * @param {String} param - * @param {Number} idx - * @param {String[]} all - array of all params, in case this is being called while parsing multiple params. * @returns {{key: String|null, value: String|null}} */ parseSingle: function (param, idx, all) { // helps handle weird edge cases such as "/get?a=b&&" if (param === EMPTY && // if param is empty _.isNumber(idx) && // this and the next condition ensures that this is part of a map call _.isArray(all) && idx !== (all && (all.length - 1))) { // not last parameter in the array return { key: null, value: null }; } var index = (typeof param === STRING) ? param.indexOf(EQUALS) : -1, paramObj = {}; // this means that there was no value for this key (not even blank, so we store this info) and the value is set // to null if (index < 0) { paramObj.key = param.substr(0, param.length); paramObj.value = null; } else { paramObj.key = param.substr(0, index); paramObj.value = param.substr(index + 1); } return paramObj; }, /** * Create a query string from array of parameters (or object of key-values). * * @note Disabled parameters are excluded. * * @param {Array|Object} params - * @returns {String} */ unparse: function (params) { if (!params) { return EMPTY; } var str, firstEnabledParam = true; // Convert hash maps to an array of params if (!_.isArray(params) && !PropertyList.isPropertyList(params)) { return _.reduce(params, function (result, value, key) { result && (result += AMPERSAND); return result + QueryParam.unparseSingle({ key, value }); }, EMPTY); } // construct a query parameter string from the list, with considerations for disabled values str = params.reduce(function (result, param) { // bail out if param is disabled if (param.disabled === true) { return result; } // don't add '&' for the very first enabled param if (firstEnabledParam) { firstEnabledParam = false; } // add '&' before concatenating param else { result += AMPERSAND; } return result + QueryParam.unparseSingle(param); }, EMPTY); return str; }, /** * Takes a query param and converts to string * * @param {Object} obj - * @returns {String} */ unparseSingle: function (obj) { if (!obj) { return EMPTY; } var key = obj.key, value = obj.value, result; if (typeof key === STRING) { result = normalizeParam(key, true); } else { result = E; } if (typeof value === STRING) { result += EQUALS + normalizeParam(value); } return result; } }); module.exports = { QueryParam };