/* global attachEvent */ const XMLHttpRequest = require("xmlhttprequest-ssl"); const Polling = require("./polling"); const Emitter = require("component-emitter"); const { pick } = require("../util"); const globalThis = require("../globalThis"); const debug = require("debug")("engine.io-client:polling-xhr"); /** * Empty function */ function empty() {} const hasXHR2 = (function() { const XMLHttpRequest = require("xmlhttprequest-ssl"); const xhr = new XMLHttpRequest({ xdomain: false }); return null != xhr.responseType; })(); class XHR extends Polling { /** * XHR Polling constructor. * * @param {Object} opts * @api public */ constructor(opts) { super(opts); if (typeof location !== "undefined") { const isSSL = "https:" === location.protocol; let port = location.port; // some user agents have empty `location.port` if (!port) { port = isSSL ? 443 : 80; } this.xd = (typeof location !== "undefined" && opts.hostname !== location.hostname) || port !== opts.port; this.xs = opts.secure !== isSSL; } /** * XHR supports binary */ const forceBase64 = opts && opts.forceBase64; this.supportsBinary = hasXHR2 && !forceBase64; } /** * Creates a request. * * @param {String} method * @api private */ request(opts = {}) { Object.assign(opts, { xd: this.xd, xs: this.xs }, this.opts); return new Request(this.uri(), opts); } /** * Sends data. * * @param {String} data to send. * @param {Function} called upon flush. * @api private */ doWrite(data, fn) { const req = this.request({ method: "POST", data: data }); const self = this; req.on("success", fn); req.on("error", function(err) { self.onError("xhr post error", err); }); } /** * Starts a poll cycle. * * @api private */ doPoll() { debug("xhr poll"); const req = this.request(); const self = this; req.on("data", function(data) { self.onData(data); }); req.on("error", function(err) { self.onError("xhr poll error", err); }); this.pollXhr = req; } } class Request extends Emitter { /** * Request constructor * * @param {Object} options * @api public */ constructor(uri, opts) { super(); this.opts = opts; this.method = opts.method || "GET"; this.uri = uri; this.async = false !== opts.async; this.data = undefined !== opts.data ? opts.data : null; this.create(); } /** * Creates the XHR object and sends the request. * * @api private */ create() { const opts = pick( this.opts, "agent", "enablesXDR", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized" ); opts.xdomain = !!this.opts.xd; opts.xscheme = !!this.opts.xs; const xhr = (this.xhr = new XMLHttpRequest(opts)); const self = this; try { debug("xhr open %s: %s", this.method, this.uri); xhr.open(this.method, this.uri, this.async); try { if (this.opts.extraHeaders) { xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true); for (let i in this.opts.extraHeaders) { if (this.opts.extraHeaders.hasOwnProperty(i)) { xhr.setRequestHeader(i, this.opts.extraHeaders[i]); } } } } catch (e) {} if ("POST" === this.method) { try { xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8"); } catch (e) {} } try { xhr.setRequestHeader("Accept", "*/*"); } catch (e) {} // ie6 check if ("withCredentials" in xhr) { xhr.withCredentials = this.opts.withCredentials; } if (this.opts.requestTimeout) { xhr.timeout = this.opts.requestTimeout; } if (this.hasXDR()) { xhr.onload = function() { self.onLoad(); }; xhr.onerror = function() { self.onError(xhr.responseText); }; } else { xhr.onreadystatechange = function() { if (4 !== xhr.readyState) return; if (200 === xhr.status || 1223 === xhr.status) { self.onLoad(); } else { // make sure the `error` event handler that's user-set // does not throw in the same tick and gets caught here setTimeout(function() { self.onError(typeof xhr.status === "number" ? xhr.status : 0); }, 0); } }; } debug("xhr data %s", this.data); xhr.send(this.data); } catch (e) { // Need to defer since .create() is called directly from the constructor // and thus the 'error' event can only be only bound *after* this exception // occurs. Therefore, also, we cannot throw here at all. setTimeout(function() { self.onError(e); }, 0); return; } if (typeof document !== "undefined") { this.index = Request.requestsCount++; Request.requests[this.index] = this; } } /** * Called upon successful response. * * @api private */ onSuccess() { this.emit("success"); this.cleanup(); } /** * Called if we have data. * * @api private */ onData(data) { this.emit("data", data); this.onSuccess(); } /** * Called upon error. * * @api private */ onError(err) { this.emit("error", err); this.cleanup(true); } /** * Cleans up house. * * @api private */ cleanup(fromError) { if ("undefined" === typeof this.xhr || null === this.xhr) { return; } // xmlhttprequest if (this.hasXDR()) { this.xhr.onload = this.xhr.onerror = empty; } else { this.xhr.onreadystatechange = empty; } if (fromError) { try { this.xhr.abort(); } catch (e) {} } if (typeof document !== "undefined") { delete Request.requests[this.index]; } this.xhr = null; } /** * Called upon load. * * @api private */ onLoad() { const data = this.xhr.responseText; if (data !== null) { this.onData(data); } } /** * Check if it has XDomainRequest. * * @api private */ hasXDR() { return typeof XDomainRequest !== "undefined" && !this.xs && this.enablesXDR; } /** * Aborts the request. * * @api public */ abort() { this.cleanup(); } } /** * Aborts pending requests when unloading the window. This is needed to prevent * memory leaks (e.g. when using IE) and to ensure that no spurious error is * emitted. */ Request.requestsCount = 0; Request.requests = {}; if (typeof document !== "undefined") { if (typeof attachEvent === "function") { attachEvent("onunload", unloadHandler); } else if (typeof addEventListener === "function") { const terminationEvent = "onpagehide" in globalThis ? "pagehide" : "unload"; addEventListener(terminationEvent, unloadHandler, false); } } function unloadHandler() { for (let i in Request.requests) { if (Request.requests.hasOwnProperty(i)) { Request.requests[i].abort(); } } } module.exports = XHR; module.exports.Request = Request;