url.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. "use strict";
  2. var _ = require('../../lodash'), url_parse = require('postman-url-encoder/parser').parse, PropertyBase = require('./property-base').PropertyBase, QueryParam = require('./query-param').QueryParam, PropertyList = require('./property-list').PropertyList, VariableList = require('./variable-list').VariableList, E = '', OBJECT = 'object', STRING = 'string', FUNCTION = 'function', PROTOCOL_HTTPS = 'https', PROTOCOL_HTTP = 'http', HTTPS_PORT = '443', HTTP_PORT = '80', PATH_SEPARATOR = '/', PATH_VARIABLE_IDENTIFIER = ':', PORT_SEPARATOR = ':', DOMAIN_SEPARATOR = '.', PROTOCOL_SEPARATOR = '://', AUTH_SEPARATOR = ':', AUTH_CREDENTIALS_SEPARATOR = '@', QUERY_SEPARATOR = '?', SEARCH_SEPARATOR = '#', DEFAULT_PROTOCOL = PROTOCOL_HTTP + PROTOCOL_SEPARATOR, MATCH_1 = '$1', regexes = {
  3. trimPath: /^\/((.+))$/,
  4. splitDomain: /\.(?![^{]*\}{2})/g
  5. }, Url;
  6. _.inherit((
  7. /**
  8. * Defines a URL.
  9. *
  10. * @constructor
  11. * @extends {PropertyBase}
  12. * @param {Object|String} options
  13. */
  14. Url = function PostmanUrl(options) {
  15. // this constructor is intended to inherit and as such the super constructor is required to be executed
  16. Url.super_.apply(this, arguments);
  17. // create the url properties
  18. this.update(options);
  19. }), PropertyBase);
  20. _.assign(Url.prototype, /** @lends Url.prototype */ {
  21. /**
  22. * Set a URL.
  23. *
  24. * @draft
  25. * @param {String|Object} url
  26. */
  27. update: function (url) {
  28. !url && (url = E);
  29. var parsedUrl = _.isString(url) ? Url.parse(url) : url, auth = parsedUrl.auth, protocol = parsedUrl.protocol, port = parsedUrl.port, path = parsedUrl.path, hash = parsedUrl.hash, host = parsedUrl.host, query = parsedUrl.query, variable = parsedUrl.variable;
  30. // convert object based query string to array
  31. // @todo: create a key value parser
  32. if (query) {
  33. if (_.isString(query)) {
  34. query = QueryParam.parse(query);
  35. }
  36. if (!_.isArray(query) && _.keys(query).length) {
  37. query = _.map(_.keys(query), function (key) {
  38. return {
  39. key: key,
  40. value: query[key]
  41. };
  42. });
  43. }
  44. }
  45. // backward compatibility with path variables being storing thins with `id`
  46. if (_.isArray(variable)) {
  47. variable = _.map(variable, function (v) {
  48. _.isObject(v) && (v.key = v.key || v.id); // @todo Remove once path variables are deprecated
  49. return v;
  50. });
  51. }
  52. // expand string path name
  53. if (_.isString(path)) {
  54. path && (path = path.replace(regexes.trimPath, MATCH_1)); // remove leading slash for valid path
  55. // if path is blank string, we set it to undefined, if '/' then single blank string array
  56. path = path ? (path === PATH_SEPARATOR ? [E] : path.split(PATH_SEPARATOR)) : undefined;
  57. }
  58. // expand host string
  59. _.isString(host) && (host = host.split(regexes.splitDomain));
  60. _.assign(this, /** @lends Url.prototype */ {
  61. /**
  62. * @type {String}
  63. */
  64. auth: auth,
  65. /**
  66. * @type {String}
  67. */
  68. protocol: protocol,
  69. /**
  70. * @type {String}
  71. */
  72. port: port,
  73. /**
  74. * @type {Array<String>}
  75. */
  76. path: path,
  77. /**
  78. * @type {String}
  79. */
  80. hash: hash,
  81. /**
  82. * @type {Array<String>}
  83. */
  84. host: host,
  85. /**
  86. * @type {PropertyList<QueryParam>}
  87. *
  88. * @todo consider setting this as undefined in v4 otherwise it's
  89. * difficult to detect URL like `localhost/?`.
  90. * currently it's replying upon a single member with empty key.
  91. */
  92. query: new PropertyList(QueryParam, this, query || []),
  93. /**
  94. * @type {VariableList}
  95. */
  96. variables: new VariableList(this, variable || [])
  97. });
  98. },
  99. /**
  100. * Add query parameters to the URL.
  101. *
  102. * @param {Object|String} params Key value pairs to add to the URL.
  103. */
  104. addQueryParams: function (params) {
  105. params = _.isString(params) ? QueryParam.parse(params) : params;
  106. this.query.populate(params);
  107. },
  108. /**
  109. * Removes query parameters from the URL.
  110. *
  111. * @param {Array<QueryParam>|Array<String>|String} params Params should be an array of strings, or an array of
  112. * actual query parameters, or a string containing the parameter key.
  113. * @note Input should *not* be a query string.
  114. */
  115. removeQueryParams: function (params) {
  116. params = _.isArray(params) ? _.map(params, function (param) {
  117. return param.key ? param.key : param;
  118. }) : [params];
  119. this.query.remove(function (param) {
  120. return _.includes(params, param.key);
  121. });
  122. },
  123. /**
  124. * Unparses a {PostmanUrl} into a string.
  125. *
  126. * @deprecated Please use {@link Url#toString} instead of this
  127. * @returns {string}
  128. */
  129. getRaw: function () {
  130. return this.toString();
  131. },
  132. /**
  133. * Unparses a {PostmanUrl} into a string.
  134. *
  135. * @param {Boolean=} forceProtocol - Forces the URL to have a protocol
  136. * @returns {string}
  137. */
  138. toString: function (forceProtocol) {
  139. var rawUrl = E, protocol = this.protocol, queryString, authString;
  140. forceProtocol && !protocol && (protocol = DEFAULT_PROTOCOL);
  141. if (protocol) {
  142. rawUrl += (_.endsWith(protocol, PROTOCOL_SEPARATOR) ? protocol : protocol + PROTOCOL_SEPARATOR);
  143. }
  144. if (this.auth) {
  145. if (typeof this.auth.user === STRING) {
  146. authString = this.auth.user;
  147. }
  148. if (typeof this.auth.password === STRING) {
  149. !authString && (authString = E);
  150. authString += AUTH_SEPARATOR + this.auth.password;
  151. }
  152. if (typeof authString === STRING) {
  153. rawUrl += authString + AUTH_CREDENTIALS_SEPARATOR;
  154. }
  155. }
  156. if (this.host) {
  157. rawUrl += this.getHost();
  158. }
  159. if (typeof _.get(this.port, 'toString') === FUNCTION) {
  160. rawUrl += PORT_SEPARATOR + this.port.toString();
  161. }
  162. if (this.path) {
  163. rawUrl += this.getPath();
  164. }
  165. if (this.query && this.query.count()) {
  166. queryString = this.getQueryString({ ignoreDisabled: true });
  167. // either all the params are disabled or a single param is like { key: '' } (http://localhost?)
  168. // in that case, query separator ? must be included in the raw URL.
  169. // @todo return undefined or string from getQueryString method to distinguish
  170. // no params vs empty param.
  171. if (queryString === E) {
  172. // check if there's any enabled param, if so, set queryString to empty string
  173. // otherwise (all disabled), it will be set as undefined
  174. queryString = this.query.find(function (param) { return !(param && param.disabled); }) && E;
  175. }
  176. if (typeof queryString === STRING) {
  177. rawUrl += QUERY_SEPARATOR + queryString;
  178. }
  179. }
  180. if (typeof this.hash === STRING) {
  181. rawUrl += SEARCH_SEPARATOR + this.hash;
  182. }
  183. return rawUrl;
  184. },
  185. /**
  186. * Returns the request path, with a leading '/'.
  187. *
  188. * @param {?Boolean=} [unresolved=false]
  189. * @returns {string}
  190. *
  191. * @note deprecated variant in v3.4:
  192. * - param {Object} options
  193. * - param {Object} options.unresolved If set to true, path variables will not be processed
  194. */
  195. getPath: function (unresolved) {
  196. if (typeof unresolved === OBJECT) { // @todo discontinue in v4
  197. unresolved = unresolved.unresolved;
  198. }
  199. // for unresolved case, this is super simple as that is how raw data is stored
  200. if (unresolved) {
  201. return PATH_SEPARATOR + this.path.join(PATH_SEPARATOR);
  202. }
  203. var self = this, segments;
  204. segments = _.transform(this.path, function (res, segment) {
  205. var variable;
  206. // check if the segment has path variable prefix followed by the variable name.
  207. if (_.startsWith(segment, PATH_VARIABLE_IDENTIFIER) && segment !== PATH_VARIABLE_IDENTIFIER) {
  208. variable = self.variables.one(segment.slice(1)); // remove path variable prefix.
  209. }
  210. variable = variable && variable.valueOf && variable.valueOf();
  211. res.push(_.isString(variable) ? variable : segment);
  212. }, []);
  213. return PATH_SEPARATOR + segments.join(PATH_SEPARATOR); // add leading slash
  214. },
  215. /**
  216. * Returns the stringified query string for this URL.
  217. *
  218. * @param {?Boolean} [encode=false] - Enables URL encoding when processing the query string
  219. * @returns {String}
  220. *
  221. * @deprecated since v3.4.6, drop support for `encode`
  222. *
  223. * @note Deprecated variant of this function is as follows:
  224. * - param {?Object} [options={}]
  225. * - param {?Boolean} options.encode - Enables URL encoding when processing the query string.
  226. * - param {?Boolean} options.ignoreDisabled - Prevents disabled query parameters from showing up in the unparsed
  227. */
  228. getQueryString: function (encode) {
  229. if (!this.query.count()) {
  230. return E;
  231. }
  232. if (typeof encode === OBJECT) { // @todo discontinue in v4
  233. return QueryParam.unparse(this.query.all(), encode);
  234. }
  235. return QueryParam.unparse(this.query.all(), { encode: encode });
  236. },
  237. /**
  238. * Returns the complete path, including the query string.
  239. *
  240. * @param {?Boolean} [encodeQuery=false] - when set to `true` the query string part will be URL encoded
  241. *
  242. * @returns {*|string}
  243. * @example /something/postman?hi=notbye
  244. *
  245. * @deprecated since v3.4.6, drop support for `encodeQuery`
  246. */
  247. getPathWithQuery: function (encodeQuery) {
  248. var path = this.getPath();
  249. // check count first so that, we can ensure that ba `?` is always appended, even if a blank query string exists
  250. if (this.query.count()) {
  251. path += (QUERY_SEPARATOR + this.getQueryString(encodeQuery));
  252. }
  253. return path;
  254. },
  255. /**
  256. * Returns the host part of the URL
  257. *
  258. * @returns {string}
  259. */
  260. getHost: function () {
  261. if (!this.host) {
  262. return E;
  263. }
  264. return _.isArray(this.host) ? this.host.join(DOMAIN_SEPARATOR) : this.host.toString();
  265. },
  266. /**
  267. * Returns the host *and* port (if any), separated by a ":"
  268. *
  269. * @param {?Boolean} [forcePort=false] - forces the port to be added even for the protocol default ones (89, 443)
  270. * @returns {String}
  271. *
  272. * @note deprecated variant since v3.5
  273. * - param {Object} options
  274. * - param {Boolean} options.forcePort
  275. */
  276. getRemote: function (forcePort) {
  277. var host = this.getHost(), port = this.port && this.port.toString();
  278. // @todo remove when v4 is released and this is discontinued
  279. if (typeof forcePort === OBJECT) {
  280. forcePort = forcePort.forcePort;
  281. }
  282. if (forcePort && !port) { // this (!port) works since it assumes port as a string
  283. port = this.protocol && (this.protocol === PROTOCOL_HTTPS) ? HTTPS_PORT : HTTP_PORT;
  284. }
  285. return port ? (host + PORT_SEPARATOR + port) : host;
  286. },
  287. /**
  288. * Returns a OAuth1.0-a compatible representation of the request URL, also called "Base URL".
  289. * For details, http://oauth.net/core/1.0a/#anchor13
  290. *
  291. * todo: should we ignore the auth parameters of the URL or not? (the standard does not mention them)
  292. * we currently are.
  293. *
  294. * @private
  295. * @returns {String}
  296. *
  297. * @deprecated since v3.5 in favour of getBaseUrl
  298. * @todo discontinue in v4.0
  299. */
  300. getOAuth1BaseUrl: function () {
  301. var protocol = this.protocol || PROTOCOL_HTTP, port = this.port ? this.port.toString() : undefined, host = ((port === HTTP_PORT ||
  302. port === HTTPS_PORT ||
  303. port === undefined) && this.host.join(DOMAIN_SEPARATOR)) || (this.host.join(DOMAIN_SEPARATOR) +
  304. PORT_SEPARATOR + port), path = this.getPath();
  305. protocol = (_.endsWith(protocol, PROTOCOL_SEPARATOR) ? protocol : protocol + PROTOCOL_SEPARATOR);
  306. return protocol.toLowerCase() + host.toLowerCase() + path;
  307. }
  308. });
  309. _.assign(Url, /** @lends Url */ {
  310. /**
  311. * Defines the name of this property for internal use.
  312. * @private
  313. * @readOnly
  314. * @type {String}
  315. */
  316. _postman_propertyName: 'Url',
  317. /**
  318. * Parses a string to a PostmanUrl, decomposing the URL into it's constituent parts,
  319. * such as path, host, port, etc.
  320. *
  321. * @param {String} url
  322. * @returns {Object}
  323. */
  324. parse: function (url) {
  325. url = url_parse(url);
  326. var pathVariables, pathVariableKeys = {};
  327. if (url.auth) {
  328. url.auth = {
  329. user: url.auth[0],
  330. password: url.auth[1]
  331. };
  332. }
  333. if (url.query) {
  334. url.query = url.query.map(QueryParam.parseSingle);
  335. }
  336. // extract path variables
  337. pathVariables = _.transform(url.path, function (res, segment) {
  338. // check if the segment has path variable prefix followed by the variable name and
  339. // the variable is not already added in the list.
  340. if (_.startsWith(segment, PATH_VARIABLE_IDENTIFIER) &&
  341. segment !== PATH_VARIABLE_IDENTIFIER &&
  342. !pathVariableKeys[segment]) {
  343. pathVariableKeys[segment] = true;
  344. res.push({ key: segment.slice(1) }); // remove path variable prefix.
  345. }
  346. }, []);
  347. url.variable = pathVariables.length ? pathVariables : undefined;
  348. return url;
  349. },
  350. /**
  351. * Checks whether an object is a Url
  352. *
  353. * @param {*} obj
  354. * @returns {Boolean}
  355. */
  356. isUrl: function (obj) {
  357. return Boolean(obj) && ((obj instanceof Url) ||
  358. _.inSuperChain(obj.constructor, '_postman_propertyName', Url._postman_propertyName));
  359. }
  360. });
  361. module.exports = {
  362. Url: Url
  363. };