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