request.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "stable"
  7. };
  8. const { ns } = require("./core/namespace");
  9. const { emit } = require("./event/core");
  10. const { merge } = require("./util/object");
  11. const { stringify } = require("./querystring");
  12. const { EventTarget } = require("./event/target");
  13. const { Class } = require("./core/heritage");
  14. const { XMLHttpRequest, forceAllowThirdPartyCookie } = require("./net/xhr");
  15. const apiUtils = require("./deprecated/api-utils");
  16. const { isValidURI } = require("./url.js");
  17. const response = ns();
  18. const request = ns();
  19. // Instead of creating a new validator for each request, just make one and
  20. // reuse it.
  21. const { validateOptions, validateSingleOption } = new OptionsValidator({
  22. url: {
  23. // Also converts a URL instance to string, bug 857902
  24. map: function (url) url.toString(),
  25. ok: isValidURI
  26. },
  27. headers: {
  28. map: function (v) v || {},
  29. is: ["object"],
  30. },
  31. content: {
  32. map: function (v) v || null,
  33. is: ["string", "object", "null"],
  34. },
  35. contentType: {
  36. map: function (v) v || "application/x-www-form-urlencoded",
  37. is: ["string"],
  38. },
  39. overrideMimeType: {
  40. map: function(v) v || null,
  41. is: ["string", "null"],
  42. }
  43. });
  44. const REUSE_ERROR = "This request object has been used already. You must " +
  45. "create a new one to make a new request."
  46. // Utility function to prep the request since it's the same between
  47. // request types
  48. function runRequest(mode, target) {
  49. let source = request(target)
  50. let { xhr, url, content, contentType, headers, overrideMimeType } = source;
  51. let isGetOrHead = (mode == "GET" || mode == "HEAD");
  52. // If this request has already been used, then we can't reuse it.
  53. // Throw an error.
  54. if (xhr)
  55. throw new Error(REUSE_ERROR);
  56. xhr = source.xhr = new XMLHttpRequest();
  57. // Build the data to be set. For GET or HEAD requests, we want to append that
  58. // to the URL before opening the request.
  59. let data = stringify(content);
  60. // If the URL already has ? in it, then we want to just use &
  61. if (isGetOrHead && data)
  62. url = url + (/\?/.test(url) ? "&" : "?") + data;
  63. // open the request
  64. xhr.open(mode, url);
  65. forceAllowThirdPartyCookie(xhr);
  66. // request header must be set after open, but before send
  67. xhr.setRequestHeader("Content-Type", contentType);
  68. // set other headers
  69. Object.keys(headers).forEach(function(name) {
  70. xhr.setRequestHeader(name, headers[name]);
  71. });
  72. // set overrideMimeType
  73. if (overrideMimeType)
  74. xhr.overrideMimeType(overrideMimeType);
  75. // handle the readystate, create the response, and call the callback
  76. xhr.onreadystatechange = function onreadystatechange() {
  77. if (xhr.readyState === 4) {
  78. let response = Response(xhr);
  79. source.response = response;
  80. emit(target, 'complete', response);
  81. }
  82. };
  83. // actually send the request.
  84. // We don't want to send data on GET or HEAD requests.
  85. xhr.send(!isGetOrHead ? data : null);
  86. }
  87. const Request = Class({
  88. extends: EventTarget,
  89. initialize: function initialize(options) {
  90. // `EventTarget.initialize` will set event listeners that are named
  91. // like `onEvent` in this case `onComplete` listener will be set to
  92. // `complete` event.
  93. EventTarget.prototype.initialize.call(this, options);
  94. // Copy normalized options.
  95. merge(request(this), validateOptions(options));
  96. },
  97. get url() { return request(this).url; },
  98. set url(value) { request(this).url = validateSingleOption('url', value); },
  99. get headers() { return request(this).headers; },
  100. set headers(value) {
  101. return request(this).headers = validateSingleOption('headers', value);
  102. },
  103. get content() { return request(this).content; },
  104. set content(value) {
  105. request(this).content = validateSingleOption('content', value);
  106. },
  107. get contentType() { return request(this).contentType; },
  108. set contentType(value) {
  109. request(this).contentType = validateSingleOption('contentType', value);
  110. },
  111. get response() { return request(this).response; },
  112. delete: function() {
  113. runRequest('DELETE', this);
  114. return this;
  115. },
  116. get: function() {
  117. runRequest('GET', this);
  118. return this;
  119. },
  120. post: function() {
  121. runRequest('POST', this);
  122. return this;
  123. },
  124. put: function() {
  125. runRequest('PUT', this);
  126. return this;
  127. },
  128. head: function() {
  129. runRequest('HEAD', this);
  130. return this;
  131. }
  132. });
  133. exports.Request = Request;
  134. const Response = Class({
  135. initialize: function initialize(request) {
  136. response(this).request = request;
  137. },
  138. get text() response(this).request.responseText,
  139. get xml() {
  140. throw new Error("Sorry, the 'xml' property is no longer available. " +
  141. "see bug 611042 for more information.");
  142. },
  143. get status() response(this).request.status,
  144. get statusText() response(this).request.statusText,
  145. get json() {
  146. try {
  147. return JSON.parse(this.text);
  148. } catch(error) {
  149. return null;
  150. }
  151. },
  152. get headers() {
  153. let headers = {}, lastKey;
  154. // Since getAllResponseHeaders() will return null if there are no headers,
  155. // defend against it by defaulting to ""
  156. let rawHeaders = response(this).request.getAllResponseHeaders() || "";
  157. rawHeaders.split("\n").forEach(function (h) {
  158. // According to the HTTP spec, the header string is terminated by an empty
  159. // line, so we can just skip it.
  160. if (!h.length) {
  161. return;
  162. }
  163. let index = h.indexOf(":");
  164. // The spec allows for leading spaces, so instead of assuming a single
  165. // leading space, just trim the values.
  166. let key = h.substring(0, index).trim(),
  167. val = h.substring(index + 1).trim();
  168. // For empty keys, that means that the header value spanned multiple lines.
  169. // In that case we should append the value to the value of lastKey with a
  170. // new line. We'll assume lastKey will be set because there should never
  171. // be an empty key on the first pass.
  172. if (key) {
  173. headers[key] = val;
  174. lastKey = key;
  175. }
  176. else {
  177. headers[lastKey] += "\n" + val;
  178. }
  179. });
  180. return headers;
  181. }
  182. });
  183. // apiUtils.validateOptions doesn't give the ability to easily validate single
  184. // options, so this is a wrapper that provides that ability.
  185. function OptionsValidator(rules) {
  186. return {
  187. validateOptions: function (options) {
  188. return apiUtils.validateOptions(options, rules);
  189. },
  190. validateSingleOption: function (field, value) {
  191. // We need to create a single rule object from our listed rules. To avoid
  192. // JavaScript String warnings, check for the field & default to an empty object.
  193. let singleRule = {};
  194. if (field in rules) {
  195. singleRule[field] = rules[field];
  196. }
  197. let singleOption = {};
  198. singleOption[field] = value;
  199. // This should throw if it's invalid, which will bubble up & out.
  200. return apiUtils.validateOptions(singleOption, singleRule)[field];
  201. }
  202. };
  203. }