"use strict"; const HTTP_STATUS_CODES = require("http").STATUS_CODES; const { spawnSync } = require("child_process"); const { URL } = require("whatwg-url"); const whatwgEncoding = require("whatwg-encoding"); const tough = require("tough-cookie"); const MIMEType = require("whatwg-mimetype"); const xhrUtils = require("./xhr-utils"); const DOMException = require("domexception/webidl2js-wrapper"); const { documentBaseURLSerialized } = require("../helpers/document-base-url"); const { asciiCaseInsensitiveMatch } = require("../helpers/strings"); const idlUtils = require("../generated/utils"); const Document = require("../generated/Document"); const Blob = require("../generated/Blob"); const FormData = require("../generated/FormData"); const XMLHttpRequestEventTargetImpl = require("./XMLHttpRequestEventTarget-impl").implementation; const XMLHttpRequestUpload = require("../generated/XMLHttpRequestUpload"); const ProgressEvent = require("../generated/ProgressEvent"); const { isArrayBuffer } = require("../generated/utils"); const { parseIntoDocument } = require("../../browser/parser"); const { fragmentSerialization } = require("../domparsing/serialization"); const { setupForSimpleEventAccessors } = require("../helpers/create-event-accessor"); const { parseJSONFromBytes } = require("../helpers/json"); const { fireAnEvent } = require("../helpers/events"); const { copyToArrayBufferInNewRealm } = require("../helpers/binary-data"); const { READY_STATES } = xhrUtils; const syncWorkerFile = require.resolve ? require.resolve("./xhr-sync-worker.js") : null; const tokenRegexp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; const fieldValueRegexp = /^[ \t]*(?:[\x21-\x7E\x80-\xFF](?:[ \t][\x21-\x7E\x80-\xFF])?)*[ \t]*$/; const forbiddenRequestHeaders = new Set([ "accept-charset", "accept-encoding", "access-control-request-headers", "access-control-request-method", "connection", "content-length", "cookie", "cookie2", "date", "dnt", "expect", "host", "keep-alive", "origin", "referer", "te", "trailer", "transfer-encoding", "upgrade", "via" ]); const forbiddenResponseHeaders = new Set([ "set-cookie", "set-cookie2" ]); const uniqueResponseHeaders = new Set([ "content-type", "content-length", "user-agent", "referer", "host", "authorization", "proxy-authorization", "if-modified-since", "if-unmodified-since", "from", "location", "max-forwards" ]); const corsSafeResponseHeaders = new Set([ "cache-control", "content-language", "content-length", "content-type", "expires", "last-modified", "pragma" ]); const allowedRequestMethods = new Set(["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE"]); const forbiddenRequestMethods = new Set(["TRACK", "TRACE", "CONNECT"]); class XMLHttpRequestImpl extends XMLHttpRequestEventTargetImpl { constructor(window) { super(window); // Avoid running `_ownerDocument` getter multiple times in the constructor: const { _ownerDocument } = this; this.upload = XMLHttpRequestUpload.createImpl(window); this.readyState = READY_STATES.UNSENT; this.responseURL = ""; this.status = 0; this.statusText = ""; this.flag = { synchronous: false, withCredentials: false, mimeType: null, auth: null, method: undefined, responseType: "", requestHeaders: {}, referrer: _ownerDocument.URL, uri: "", timeout: 0, body: undefined, formData: false, preflight: false, requestManager: _ownerDocument._requestManager, strictSSL: window._resourceLoader._strictSSL, proxy: window._resourceLoader._proxy, cookieJar: _ownerDocument._cookieJar, encoding: _ownerDocument._encoding, origin: window._origin, userAgent: window.navigator.userAgent }; this.properties = { beforeSend: false, send: false, client: null, timeoutStart: 0, timeoutId: 0, timeoutFn: null, responseBuffer: null, responseCache: null, responseTextCache: null, responseXMLCache: null, responseHeaders: {}, filteredResponseHeaders: [], error: "", uploadComplete: false, uploadListener: false, // Signifies that we're calling abort() from xhr-utils.js because of a window shutdown. // In that case the termination reason is "fatal", not "end-user abort". abortError: false, cookieJar: _ownerDocument._cookieJar, bufferStepSize: 1 * 1024 * 1024, // pre-allocate buffer increase step size. init value is 1MB totalReceivedChunkSize: 0 }; } get responseType() { return this.flag.responseType; } set responseType(responseType) { const { flag } = this; if (this.readyState === READY_STATES.LOADING || this.readyState === READY_STATES.DONE) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState === READY_STATES.OPENED && flag.synchronous) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } flag.responseType = responseType; } get response() { const { properties } = this; if (properties.responseCache) { // Needed because of: https://github.com/jsdom/webidl2js/issues/149 return idlUtils.tryWrapperForImpl(properties.responseCache); } let res; const responseBuffer = properties.responseBuffer ? properties.responseBuffer.slice(0, properties.totalReceivedChunkSize) : null; switch (this.responseType) { case "": case "text": { res = this.responseText; break; } case "arraybuffer": { if (!responseBuffer) { return null; } res = copyToArrayBufferInNewRealm(responseBuffer, this._globalObject); break; } case "blob": { if (!responseBuffer) { return null; } const contentType = finalMIMEType(this); res = Blob.createImpl(this._globalObject, [ [new Uint8Array(responseBuffer)], { type: contentType || "" } ]); break; } case "document": { res = this.responseXML; break; } case "json": { if (this.readyState !== READY_STATES.DONE || !responseBuffer) { res = null; } try { res = parseJSONFromBytes(responseBuffer); } catch (e) { res = null; } break; } } properties.responseCache = res; // Needed because of: https://github.com/jsdom/webidl2js/issues/149 return idlUtils.tryWrapperForImpl(res); } get responseText() { const { properties } = this; if (this.responseType !== "" && this.responseType !== "text") { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState !== READY_STATES.LOADING && this.readyState !== READY_STATES.DONE) { return ""; } if (properties.responseTextCache) { return properties.responseTextCache; } const responseBuffer = properties.responseBuffer ? properties.responseBuffer.slice(0, properties.totalReceivedChunkSize) : null; if (!responseBuffer) { return ""; } const fallbackEncoding = finalCharset(this) || whatwgEncoding.getBOMEncoding(responseBuffer) || "UTF-8"; const res = whatwgEncoding.decode(responseBuffer, fallbackEncoding); properties.responseTextCache = res; return res; } get responseXML() { const { flag, properties } = this; if (this.responseType !== "" && this.responseType !== "document") { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState !== READY_STATES.DONE) { return null; } if (properties.responseXMLCache) { return properties.responseXMLCache; } const responseBuffer = properties.responseBuffer ? properties.responseBuffer.slice(0, properties.totalReceivedChunkSize) : null; if (!responseBuffer) { return null; } const contentType = finalMIMEType(this); let isHTML = false; let isXML = false; const parsed = MIMEType.parse(contentType); if (parsed) { isHTML = parsed.isHTML(); isXML = parsed.isXML(); if (!isXML && !isHTML) { return null; } } if (this.responseType === "" && isHTML) { return null; } const encoding = finalCharset(this) || whatwgEncoding.getBOMEncoding(responseBuffer) || "UTF-8"; const resText = whatwgEncoding.decode(responseBuffer, encoding); if (!resText) { return null; } const res = Document.createImpl(this._globalObject, [], { options: { url: flag.uri, lastModified: new Date(getResponseHeader(this, "last-modified")), parsingMode: isHTML ? "html" : "xml", cookieJar: { setCookieSync: () => undefined, getCookieStringSync: () => "" }, encoding, parseOptions: this._ownerDocument._parseOptions } }); try { parseIntoDocument(resText, res); } catch (e) { properties.responseXMLCache = null; return null; } res.close(); properties.responseXMLCache = res; return res; } get timeout() { return this.flag.timeout; } set timeout(val) { const { flag, properties } = this; if (flag.synchronous) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } flag.timeout = val; clearTimeout(properties.timeoutId); if (val > 0 && properties.timeoutFn) { properties.timeoutId = setTimeout( properties.timeoutFn, Math.max(0, val - ((new Date()).getTime() - properties.timeoutStart)) ); } else { properties.timeoutFn = null; properties.timeoutStart = 0; } } get withCredentials() { return this.flag.withCredentials; } set withCredentials(val) { const { flag, properties } = this; if (!(this.readyState === READY_STATES.UNSENT || this.readyState === READY_STATES.OPENED)) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (properties.send) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } flag.withCredentials = val; } abort() { const { properties } = this; // Terminate the request clearTimeout(properties.timeoutId); properties.timeoutFn = null; properties.timeoutStart = 0; const { client } = properties; if (client) { client.abort(); properties.client = null; } if (properties.abortError) { // Special case that ideally shouldn't be going through the public API at all. // Run the https://xhr.spec.whatwg.org/#handle-errors "fatal" steps. this.readyState = READY_STATES.DONE; properties.send = false; xhrUtils.setResponseToNetworkError(this); return; } if ((this.readyState === READY_STATES.OPENED && properties.send) || this.readyState === READY_STATES.HEADERS_RECEIVED || this.readyState === READY_STATES.LOADING) { xhrUtils.requestErrorSteps(this, "abort"); } if (this.readyState === READY_STATES.DONE) { this.readyState = READY_STATES.UNSENT; xhrUtils.setResponseToNetworkError(this); } } getAllResponseHeaders() { const { properties, readyState } = this; if (readyState === READY_STATES.UNSENT || readyState === READY_STATES.OPENED) { return ""; } return Object.keys(properties.responseHeaders) .filter(key => properties.filteredResponseHeaders.indexOf(key) === -1) .map(key => [key.toLowerCase(), properties.responseHeaders[key]].join(": ")) .join("\r\n"); } getResponseHeader(header) { const { properties, readyState } = this; if (readyState === READY_STATES.UNSENT || readyState === READY_STATES.OPENED) { return null; } const lcHeader = header.toLowerCase(); if (properties.filteredResponseHeaders.find(filtered => lcHeader === filtered.toLowerCase())) { return null; } return getResponseHeader(this, lcHeader); } open(method, uri, asynchronous, user, password) { const { flag, properties, _ownerDocument } = this; if (!_ownerDocument) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (!tokenRegexp.test(method)) { throw DOMException.create(this._globalObject, [ "The string did not match the expected pattern.", "SyntaxError" ]); } const upperCaseMethod = method.toUpperCase(); if (forbiddenRequestMethods.has(upperCaseMethod)) { throw DOMException.create(this._globalObject, ["The operation is insecure.", "SecurityError"]); } const { client } = properties; if (client && typeof client.abort === "function") { client.abort(); } if (allowedRequestMethods.has(upperCaseMethod)) { method = upperCaseMethod; } if (typeof asynchronous !== "undefined") { flag.synchronous = !asynchronous; } else { flag.synchronous = false; } if (flag.responseType && flag.synchronous) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } if (flag.synchronous && flag.timeout) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } flag.method = method; let urlObj; try { urlObj = new URL(uri, documentBaseURLSerialized(_ownerDocument)); } catch (e) { throw DOMException.create(this._globalObject, [ "The string did not match the expected pattern.", "SyntaxError" ]); } if (user || (password && !urlObj.username)) { flag.auth = { user, pass: password }; urlObj.username = ""; urlObj.password = ""; } flag.uri = urlObj.href; flag.requestHeaders = {}; flag.preflight = false; properties.send = false; properties.uploadListener = false; properties.abortError = false; this.responseURL = ""; readyStateChange(this, READY_STATES.OPENED); } overrideMimeType(mime) { const { readyState } = this; if (readyState === READY_STATES.LOADING || readyState === READY_STATES.DONE) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } this.flag.overrideMIMEType = "application/octet-stream"; // Waiting for better spec: https://github.com/whatwg/xhr/issues/157 const parsed = MIMEType.parse(mime); if (parsed) { this.flag.overrideMIMEType = parsed.essence; const charset = parsed.parameters.get("charset"); if (charset) { this.flag.overrideCharset = whatwgEncoding.labelToName(charset); } } } // TODO: Add support for URLSearchParams and ReadableStream send(body) { const { flag, properties, upload, _ownerDocument } = this; // Not per spec, but per tests: https://github.com/whatwg/xhr/issues/65 if (!_ownerDocument) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState !== READY_STATES.OPENED || properties.send) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } properties.beforeSend = true; try { if (flag.method === "GET" || flag.method === "HEAD") { body = null; } if (body !== null) { let encoding = null; let mimeType = null; if (Document.isImpl(body)) { encoding = "UTF-8"; mimeType = (body._parsingMode === "html" ? "text/html" : "application/xml") + ";charset=UTF-8"; flag.body = fragmentSerialization(body, { requireWellFormed: false }); } else { if (typeof body === "string") { encoding = "UTF-8"; } const { buffer, formData, contentType } = extractBody(body); mimeType = contentType; flag.body = buffer || formData; flag.formData = Boolean(formData); } const existingContentType = xhrUtils.getRequestHeader(flag.requestHeaders, "content-type"); if (mimeType !== null && existingContentType === null) { flag.requestHeaders["Content-Type"] = mimeType; } else if (existingContentType !== null && encoding !== null) { // Waiting for better spec: https://github.com/whatwg/xhr/issues/188. This seems like a good guess at what // the spec will be, in the meantime. const parsed = MIMEType.parse(existingContentType); if (parsed) { const charset = parsed.parameters.get("charset"); if (charset && !asciiCaseInsensitiveMatch(charset, encoding) && encoding !== null) { parsed.parameters.set("charset", encoding); xhrUtils.updateRequestHeader(flag.requestHeaders, "content-type", parsed.toString()); } } } } } finally { if (properties.beforeSend) { properties.beforeSend = false; } else { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } } if (Object.keys(upload._eventListeners).length > 0) { properties.uploadListener = true; } // request doesn't like zero-length bodies if (flag.body && flag.body.byteLength === 0) { flag.body = null; } if (flag.synchronous) { const flagStr = JSON.stringify(flag, function (k, v) { if (this === flag && k === "requestManager") { return null; } if (this === flag && k === "pool" && v) { return { maxSockets: v.maxSockets }; } return v; }); const res = spawnSync( process.execPath, [syncWorkerFile], { input: flagStr, maxBuffer: Infinity } ); if (res.status !== 0) { throw new Error(res.stderr.toString()); } if (res.error) { if (typeof res.error === "string") { res.error = new Error(res.error); } throw res.error; } const response = JSON.parse(res.stdout.toString()); const resProp = response.properties; if (resProp.responseBuffer && resProp.responseBuffer.data) { resProp.responseBuffer = Buffer.from(resProp.responseBuffer.data); } if (resProp.cookieJar) { resProp.cookieJar = tough.CookieJar.deserializeSync( resProp.cookieJar, _ownerDocument._cookieJar.store ); } this.readyState = READY_STATES.LOADING; this.status = response.status; this.statusText = response.statusText; this.responseURL = response.responseURL; Object.assign(this.properties, response.properties); if (resProp.error) { xhrUtils.dispatchError(this); throw DOMException.create(this._globalObject, [resProp.error, "NetworkError"]); } else { const { responseBuffer } = properties; const contentLength = getResponseHeader(this, "content-length") || "0"; const bufferLength = parseInt(contentLength) || responseBuffer.length; const progressObj = { lengthComputable: false }; if (bufferLength !== 0) { progressObj.total = bufferLength; progressObj.loaded = bufferLength; progressObj.lengthComputable = true; } fireAnEvent("progress", this, ProgressEvent, progressObj); readyStateChange(this, READY_STATES.DONE); fireAnEvent("load", this, ProgressEvent, progressObj); fireAnEvent("loadend", this, ProgressEvent, progressObj); } } else { properties.send = true; fireAnEvent("loadstart", this, ProgressEvent); const client = xhrUtils.createClient(this); properties.client = client; // For new client, reset totalReceivedChunkSize and bufferStepSize properties.totalReceivedChunkSize = 0; properties.bufferStepSize = 1 * 1024 * 1024; properties.origin = flag.origin; client.on("error", err => { client.removeAllListeners(); properties.error = err; xhrUtils.dispatchError(this); }); client.on("response", (res, url) => receiveResponse(this, res, url)); client.on("redirect", (response, requestHeaders, currentURL) => { const destUrlObj = new URL(requestHeaders.Referer); const urlObj = new URL(currentURL); if (destUrlObj.origin !== urlObj.origin && destUrlObj.origin !== flag.origin) { properties.origin = "null"; } requestHeaders.Origin = properties.origin; if (flag.origin !== destUrlObj.origin && destUrlObj.protocol !== "data:") { if (!xhrUtils.validCORSHeaders(this, response, flag, properties, flag.origin)) { return; } if (urlObj.username || urlObj.password) { properties.error = "Userinfo forbidden in cors redirect"; xhrUtils.dispatchError(this); } } }); if (body !== null && body !== "") { properties.uploadComplete = false; setDispatchProgressEvents(this); } else { properties.uploadComplete = true; } if (this.timeout > 0) { properties.timeoutStart = (new Date()).getTime(); properties.timeoutFn = () => { client.abort(); if (!(this.readyState === READY_STATES.UNSENT || (this.readyState === READY_STATES.OPENED && !properties.send) || this.readyState === READY_STATES.DONE)) { properties.send = false; let stateChanged = false; if (!properties.uploadComplete) { fireAnEvent("progress", upload, ProgressEvent); readyStateChange(this, READY_STATES.DONE); fireAnEvent("timeout", upload, ProgressEvent); fireAnEvent("loadend", upload, ProgressEvent); stateChanged = true; } fireAnEvent("progress", this, ProgressEvent); if (!stateChanged) { readyStateChange(this, READY_STATES.DONE); } fireAnEvent("timeout", this, ProgressEvent); fireAnEvent("loadend", this, ProgressEvent); } this.readyState = READY_STATES.UNSENT; }; properties.timeoutId = setTimeout(properties.timeoutFn, this.timeout); } } } setRequestHeader(header, value) { const { flag, properties } = this; if (this.readyState !== READY_STATES.OPENED || properties.send) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } value = normalizeHeaderValue(value); if (!tokenRegexp.test(header) || !fieldValueRegexp.test(value)) { throw DOMException.create(this._globalObject, [ "The string did not match the expected pattern.", "SyntaxError" ]); } const lcHeader = header.toLowerCase(); if (forbiddenRequestHeaders.has(lcHeader) || lcHeader.startsWith("sec-") || lcHeader.startsWith("proxy-")) { return; } const keys = Object.keys(flag.requestHeaders); let n = keys.length; while (n--) { const key = keys[n]; if (key.toLowerCase() === lcHeader) { flag.requestHeaders[key] += ", " + value; return; } } flag.requestHeaders[header] = value; } } setupForSimpleEventAccessors(XMLHttpRequestImpl.prototype, ["readystatechange"]); function readyStateChange(xhr, readyState) { if (xhr.readyState === readyState) { return; } xhr.readyState = readyState; fireAnEvent("readystatechange", xhr); } function receiveResponse(xhr, response, currentURL) { const { flag, properties } = xhr; const { rawHeaders, statusCode } = response; let byteOffset = 0; const headers = {}; const filteredResponseHeaders = []; const headerMap = {}; const n = Number(rawHeaders.length); for (let i = 0; i < n; i += 2) { const k = rawHeaders[i]; const kl = k.toLowerCase(); const v = rawHeaders[i + 1]; if (uniqueResponseHeaders.has(kl)) { if (headerMap[kl] !== undefined) { delete headers[headerMap[kl]]; } headers[k] = v; } else if (headerMap[kl] !== undefined) { headers[headerMap[kl]] += ", " + v; } else { headers[k] = v; } headerMap[kl] = k; } const destUrlObj = new URL(currentURL); if (properties.origin !== destUrlObj.origin && destUrlObj.protocol !== "data:") { if (!xhrUtils.validCORSHeaders(xhr, response, flag, properties, properties.origin)) { return; } const acehStr = response.headers["access-control-expose-headers"]; const aceh = new Set(acehStr ? acehStr.trim().toLowerCase().split(xhrUtils.headerListSeparatorRegexp) : []); for (const header in headers) { const lcHeader = header.toLowerCase(); if (!corsSafeResponseHeaders.has(lcHeader) && !aceh.has(lcHeader)) { filteredResponseHeaders.push(header); } } } for (const header in headers) { const lcHeader = header.toLowerCase(); if (forbiddenResponseHeaders.has(lcHeader)) { filteredResponseHeaders.push(header); } } xhr.responseURL = destUrlObj.href; xhr.status = statusCode; xhr.statusText = response.statusMessage || HTTP_STATUS_CODES[statusCode] || ""; properties.responseHeaders = headers; properties.filteredResponseHeaders = filteredResponseHeaders; const contentLength = getResponseHeader(xhr, "content-length") || "0"; const bufferLength = parseInt(contentLength) || 0; const progressObj = { lengthComputable: false }; let lastProgressReported; if (bufferLength !== 0) { progressObj.total = bufferLength; progressObj.loaded = 0; progressObj.lengthComputable = true; } // pre-allocate buffer. properties.responseBuffer = Buffer.alloc(properties.bufferStepSize); properties.responseCache = null; properties.responseTextCache = null; properties.responseXMLCache = null; readyStateChange(xhr, READY_STATES.HEADERS_RECEIVED); if (!properties.client) { // The request was aborted in reaction to the readystatechange event. return; } // Can't use the client since the client gets the post-ungzipping bytes (which can be greater than the // Content-Length). response.on("data", chunk => { byteOffset += chunk.length; progressObj.loaded = byteOffset; }); properties.client.on("data", chunk => { properties.totalReceivedChunkSize += chunk.length; if (properties.totalReceivedChunkSize >= properties.bufferStepSize) { properties.bufferStepSize *= 2; while (properties.totalReceivedChunkSize >= properties.bufferStepSize) { properties.bufferStepSize *= 2; } const tmpBuf = Buffer.alloc(properties.bufferStepSize); properties.responseBuffer.copy(tmpBuf, 0, 0, properties.responseBuffer.length); properties.responseBuffer = tmpBuf; } chunk.copy(properties.responseBuffer, properties.totalReceivedChunkSize - chunk.length, 0, chunk.length); properties.responseCache = null; properties.responseTextCache = null; properties.responseXMLCache = null; if (xhr.readyState === READY_STATES.HEADERS_RECEIVED) { xhr.readyState = READY_STATES.LOADING; } fireAnEvent("readystatechange", xhr); if (progressObj.total !== progressObj.loaded || properties.totalReceivedChunkSize === byteOffset) { if (lastProgressReported !== progressObj.loaded) { // This is a necessary check in the gzip case where we can be getting new data from the client, as it // un-gzips, but no new data has been gotten from the response, so we should not fire a progress event. lastProgressReported = progressObj.loaded; fireAnEvent("progress", xhr, ProgressEvent, progressObj); } } }); properties.client.on("end", () => { clearTimeout(properties.timeoutId); properties.timeoutFn = null; properties.timeoutStart = 0; properties.client = null; if (lastProgressReported !== progressObj.loaded) { // https://github.com/whatwg/xhr/issues/318 fireAnEvent("progress", xhr, ProgressEvent, progressObj); } readyStateChange(xhr, READY_STATES.DONE); fireAnEvent("load", xhr, ProgressEvent, progressObj); fireAnEvent("loadend", xhr, ProgressEvent, progressObj); }); } function setDispatchProgressEvents(xhr) { const { properties, upload } = xhr; const { client } = properties; let total = 0; let lengthComputable = false; const length = client.headers && parseInt(xhrUtils.getRequestHeader(client.headers, "content-length")); if (length) { total = length; lengthComputable = true; } const initProgress = { lengthComputable, total, loaded: 0 }; if (properties.uploadListener) { fireAnEvent("loadstart", upload, ProgressEvent, initProgress); } client.on("request", req => { req.on("response", () => { properties.uploadComplete = true; if (!properties.uploadListener) { return; } const progress = { lengthComputable, total, loaded: total }; fireAnEvent("progress", upload, ProgressEvent, progress); fireAnEvent("load", upload, ProgressEvent, progress); fireAnEvent("loadend", upload, ProgressEvent, progress); }); }); } function finalMIMEType(xhr) { const { flag } = xhr; return flag.overrideMIMEType || getResponseHeader(xhr, "content-type"); } function finalCharset(xhr) { const { flag } = xhr; if (flag.overrideCharset) { return flag.overrideCharset; } const parsedContentType = MIMEType.parse(getResponseHeader(xhr, "content-type")); if (parsedContentType) { return whatwgEncoding.labelToName(parsedContentType.parameters.get("charset")); } return null; } function getResponseHeader(xhr, lcHeader) { const { properties } = xhr; const keys = Object.keys(properties.responseHeaders); let n = keys.length; while (n--) { const key = keys[n]; if (key.toLowerCase() === lcHeader) { return properties.responseHeaders[key]; } } return null; } function normalizeHeaderValue(value) { return value.replace(/^[\x09\x0A\x0D\x20]+/, "").replace(/[\x09\x0A\x0D\x20]+$/, ""); } function extractBody(bodyInit) { // https://fetch.spec.whatwg.org/#concept-bodyinit-extract // except we represent the body as a Node.js Buffer instead, // or a special case for FormData since we want request to handle that. Probably it would be // cleaner (and allow a future without request) if we did the form encoding ourself. if (Blob.isImpl(bodyInit)) { return { buffer: bodyInit._buffer, contentType: bodyInit.type === "" ? null : bodyInit.type }; } else if (isArrayBuffer(bodyInit)) { return { buffer: Buffer.from(bodyInit), contentType: null }; } else if (ArrayBuffer.isView(bodyInit)) { return { buffer: Buffer.from(bodyInit.buffer, bodyInit.byteOffset, bodyInit.byteLength), contentType: null }; } else if (FormData.isImpl(bodyInit)) { const formData = []; for (const entry of bodyInit._entries) { let val; if (Blob.isImpl(entry.value)) { const blob = entry.value; val = { name: entry.name, value: blob._buffer, options: { filename: blob.name, contentType: blob.type, knownLength: blob.size } }; } else { val = entry; } formData.push(val); } return { formData }; } // Must be a string return { buffer: Buffer.from(bodyInit, "utf-8"), contentType: "text/plain;charset=UTF-8" }; } exports.implementation = XMLHttpRequestImpl;