43f529c41c1ec4fa44fce0dcb2eb4e8ec8d17ecc
[platform/framework/web/wrtjs.git] /
1 /* global attachEvent */
2
3 /**
4  * Module requirements.
5  */
6
7 var XMLHttpRequest = require('xmlhttprequest-ssl');
8 var Polling = require('./polling');
9 var Emitter = require('component-emitter');
10 var inherit = require('component-inherit');
11 var debug = require('debug')('engine.io-client:polling-xhr');
12 var globalThis = require('../globalThis');
13
14 /**
15  * Module exports.
16  */
17
18 module.exports = XHR;
19 module.exports.Request = Request;
20
21 /**
22  * Empty function
23  */
24
25 function empty () {}
26
27 /**
28  * XHR Polling constructor.
29  *
30  * @param {Object} opts
31  * @api public
32  */
33
34 function XHR (opts) {
35   Polling.call(this, opts);
36   this.requestTimeout = opts.requestTimeout;
37   this.extraHeaders = opts.extraHeaders;
38
39   if (typeof location !== 'undefined') {
40     var isSSL = 'https:' === location.protocol;
41     var port = location.port;
42
43     // some user agents have empty `location.port`
44     if (!port) {
45       port = isSSL ? 443 : 80;
46     }
47
48     this.xd = (typeof location !== 'undefined' && opts.hostname !== location.hostname) ||
49       port !== opts.port;
50     this.xs = opts.secure !== isSSL;
51   }
52 }
53
54 /**
55  * Inherits from Polling.
56  */
57
58 inherit(XHR, Polling);
59
60 /**
61  * XHR supports binary
62  */
63
64 XHR.prototype.supportsBinary = true;
65
66 /**
67  * Creates a request.
68  *
69  * @param {String} method
70  * @api private
71  */
72
73 XHR.prototype.request = function (opts) {
74   opts = opts || {};
75   opts.uri = this.uri();
76   opts.xd = this.xd;
77   opts.xs = this.xs;
78   opts.agent = this.agent || false;
79   opts.supportsBinary = this.supportsBinary;
80   opts.enablesXDR = this.enablesXDR;
81   opts.withCredentials = this.withCredentials;
82
83   // SSL options for Node.js client
84   opts.pfx = this.pfx;
85   opts.key = this.key;
86   opts.passphrase = this.passphrase;
87   opts.cert = this.cert;
88   opts.ca = this.ca;
89   opts.ciphers = this.ciphers;
90   opts.rejectUnauthorized = this.rejectUnauthorized;
91   opts.requestTimeout = this.requestTimeout;
92
93   // other options for Node.js client
94   opts.extraHeaders = this.extraHeaders;
95
96   return new Request(opts);
97 };
98
99 /**
100  * Sends data.
101  *
102  * @param {String} data to send.
103  * @param {Function} called upon flush.
104  * @api private
105  */
106
107 XHR.prototype.doWrite = function (data, fn) {
108   var isBinary = typeof data !== 'string' && data !== undefined;
109   var req = this.request({ method: 'POST', data: data, isBinary: isBinary });
110   var self = this;
111   req.on('success', fn);
112   req.on('error', function (err) {
113     self.onError('xhr post error', err);
114   });
115   this.sendXhr = req;
116 };
117
118 /**
119  * Starts a poll cycle.
120  *
121  * @api private
122  */
123
124 XHR.prototype.doPoll = function () {
125   debug('xhr poll');
126   var req = this.request();
127   var self = this;
128   req.on('data', function (data) {
129     self.onData(data);
130   });
131   req.on('error', function (err) {
132     self.onError('xhr poll error', err);
133   });
134   this.pollXhr = req;
135 };
136
137 /**
138  * Request constructor
139  *
140  * @param {Object} options
141  * @api public
142  */
143
144 function Request (opts) {
145   this.method = opts.method || 'GET';
146   this.uri = opts.uri;
147   this.xd = !!opts.xd;
148   this.xs = !!opts.xs;
149   this.async = false !== opts.async;
150   this.data = undefined !== opts.data ? opts.data : null;
151   this.agent = opts.agent;
152   this.isBinary = opts.isBinary;
153   this.supportsBinary = opts.supportsBinary;
154   this.enablesXDR = opts.enablesXDR;
155   this.withCredentials = opts.withCredentials;
156   this.requestTimeout = opts.requestTimeout;
157
158   // SSL options for Node.js client
159   this.pfx = opts.pfx;
160   this.key = opts.key;
161   this.passphrase = opts.passphrase;
162   this.cert = opts.cert;
163   this.ca = opts.ca;
164   this.ciphers = opts.ciphers;
165   this.rejectUnauthorized = opts.rejectUnauthorized;
166
167   // other options for Node.js client
168   this.extraHeaders = opts.extraHeaders;
169
170   this.create();
171 }
172
173 /**
174  * Mix in `Emitter`.
175  */
176
177 Emitter(Request.prototype);
178
179 /**
180  * Creates the XHR object and sends the request.
181  *
182  * @api private
183  */
184
185 Request.prototype.create = function () {
186   var opts = { agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR };
187
188   // SSL options for Node.js client
189   opts.pfx = this.pfx;
190   opts.key = this.key;
191   opts.passphrase = this.passphrase;
192   opts.cert = this.cert;
193   opts.ca = this.ca;
194   opts.ciphers = this.ciphers;
195   opts.rejectUnauthorized = this.rejectUnauthorized;
196
197   var xhr = this.xhr = new XMLHttpRequest(opts);
198   var self = this;
199
200   try {
201     debug('xhr open %s: %s', this.method, this.uri);
202     xhr.open(this.method, this.uri, this.async);
203     try {
204       if (this.extraHeaders) {
205         xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
206         for (var i in this.extraHeaders) {
207           if (this.extraHeaders.hasOwnProperty(i)) {
208             xhr.setRequestHeader(i, this.extraHeaders[i]);
209           }
210         }
211       }
212     } catch (e) {}
213
214     if ('POST' === this.method) {
215       try {
216         if (this.isBinary) {
217           xhr.setRequestHeader('Content-type', 'application/octet-stream');
218         } else {
219           xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
220         }
221       } catch (e) {}
222     }
223
224     try {
225       xhr.setRequestHeader('Accept', '*/*');
226     } catch (e) {}
227
228     // ie6 check
229     if ('withCredentials' in xhr) {
230       xhr.withCredentials = this.withCredentials;
231     }
232
233     if (this.requestTimeout) {
234       xhr.timeout = this.requestTimeout;
235     }
236
237     if (this.hasXDR()) {
238       xhr.onload = function () {
239         self.onLoad();
240       };
241       xhr.onerror = function () {
242         self.onError(xhr.responseText);
243       };
244     } else {
245       xhr.onreadystatechange = function () {
246         if (xhr.readyState === 2) {
247           try {
248             var contentType = xhr.getResponseHeader('Content-Type');
249             if (self.supportsBinary && contentType === 'application/octet-stream' || contentType === 'application/octet-stream; charset=UTF-8') {
250               xhr.responseType = 'arraybuffer';
251             }
252           } catch (e) {}
253         }
254         if (4 !== xhr.readyState) return;
255         if (200 === xhr.status || 1223 === xhr.status) {
256           self.onLoad();
257         } else {
258           // make sure the `error` event handler that's user-set
259           // does not throw in the same tick and gets caught here
260           setTimeout(function () {
261             self.onError(typeof xhr.status === 'number' ? xhr.status : 0);
262           }, 0);
263         }
264       };
265     }
266
267     debug('xhr data %s', this.data);
268     xhr.send(this.data);
269   } catch (e) {
270     // Need to defer since .create() is called directly fhrom the constructor
271     // and thus the 'error' event can only be only bound *after* this exception
272     // occurs.  Therefore, also, we cannot throw here at all.
273     setTimeout(function () {
274       self.onError(e);
275     }, 0);
276     return;
277   }
278
279   if (typeof document !== 'undefined') {
280     this.index = Request.requestsCount++;
281     Request.requests[this.index] = this;
282   }
283 };
284
285 /**
286  * Called upon successful response.
287  *
288  * @api private
289  */
290
291 Request.prototype.onSuccess = function () {
292   this.emit('success');
293   this.cleanup();
294 };
295
296 /**
297  * Called if we have data.
298  *
299  * @api private
300  */
301
302 Request.prototype.onData = function (data) {
303   this.emit('data', data);
304   this.onSuccess();
305 };
306
307 /**
308  * Called upon error.
309  *
310  * @api private
311  */
312
313 Request.prototype.onError = function (err) {
314   this.emit('error', err);
315   this.cleanup(true);
316 };
317
318 /**
319  * Cleans up house.
320  *
321  * @api private
322  */
323
324 Request.prototype.cleanup = function (fromError) {
325   if ('undefined' === typeof this.xhr || null === this.xhr) {
326     return;
327   }
328   // xmlhttprequest
329   if (this.hasXDR()) {
330     this.xhr.onload = this.xhr.onerror = empty;
331   } else {
332     this.xhr.onreadystatechange = empty;
333   }
334
335   if (fromError) {
336     try {
337       this.xhr.abort();
338     } catch (e) {}
339   }
340
341   if (typeof document !== 'undefined') {
342     delete Request.requests[this.index];
343   }
344
345   this.xhr = null;
346 };
347
348 /**
349  * Called upon load.
350  *
351  * @api private
352  */
353
354 Request.prototype.onLoad = function () {
355   var data;
356   try {
357     var contentType;
358     try {
359       contentType = this.xhr.getResponseHeader('Content-Type');
360     } catch (e) {}
361     if (contentType === 'application/octet-stream' || contentType === 'application/octet-stream; charset=UTF-8') {
362       data = this.xhr.response || this.xhr.responseText;
363     } else {
364       data = this.xhr.responseText;
365     }
366   } catch (e) {
367     this.onError(e);
368   }
369   if (null != data) {
370     this.onData(data);
371   }
372 };
373
374 /**
375  * Check if it has XDomainRequest.
376  *
377  * @api private
378  */
379
380 Request.prototype.hasXDR = function () {
381   return typeof XDomainRequest !== 'undefined' && !this.xs && this.enablesXDR;
382 };
383
384 /**
385  * Aborts the request.
386  *
387  * @api public
388  */
389
390 Request.prototype.abort = function () {
391   this.cleanup();
392 };
393
394 /**
395  * Aborts pending requests when unloading the window. This is needed to prevent
396  * memory leaks (e.g. when using IE) and to ensure that no spurious error is
397  * emitted.
398  */
399
400 Request.requestsCount = 0;
401 Request.requests = {};
402
403 if (typeof document !== 'undefined') {
404   if (typeof attachEvent === 'function') {
405     attachEvent('onunload', unloadHandler);
406   } else if (typeof addEventListener === 'function') {
407     var terminationEvent = 'onpagehide' in globalThis ? 'pagehide' : 'unload';
408     addEventListener(terminationEvent, unloadHandler, false);
409   }
410 }
411
412 function unloadHandler () {
413   for (var i in Request.requests) {
414     if (Request.requests.hasOwnProperty(i)) {
415       Request.requests[i].abort();
416     }
417   }
418 }