“use strict”;

const HTTP_STATUS_CODES = require(“http”).STATUS_CODES; const spawnSync = require(“child_process”).spawnSync; const URL = require(“whatwg-url”).URL; const whatwgEncoding = require(“whatwg-encoding”); const tough = require(“tough-cookie”); const parseContentType = require(“content-type-parser”);

const xhrUtils = require(“./xhr-utils”); const DOMException = require(“../web-idl/DOMException”); const xhrSymbols = require(“./xmlhttprequest-symbols”); const addConstants = require(“../utils”).addConstants; const documentBaseURLSerialized = require(“./helpers/document-base-url”).documentBaseURLSerialized; const idlUtils = require(“./generated/utils”); const Document = require(“./generated/Document”); const Blob = require(“./generated/Blob”); const domToHtml = require(“../browser/domtohtml”).domToHtml;

const syncWorkerFile = require.resolve ? require.resolve(“./xhr-sync-worker.js”) : null;

const tokenRegexp = /^[!#$%&'*+-.^_`|~0-9A-Za-z]+$/; const headerListSeparatorRegexp = /,[ t]*/; const fieldValueRegexp = /^[ t]*(?:[x21-x7Ex80-xFF](?:[ t][x21-x7Ex80-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-type",
"expires",
"last-modified",
"pragma"

]);

const allowedRequestMethods = new Set([“OPTIONS”, “GET”, “HEAD”, “POST”, “PUT”, “DELETE”]); const forbiddenRequestMethods = new Set([“TRACK”, “TRACE”, “CONNECT”]);

const XMLHttpRequestResponseType = new Set([

"",
"arraybuffer",
"blob",
"document",
"json",
"text"

]);

const simpleHeaders = xhrUtils.simpleHeaders;

const redirectStatuses = new Set([301, 302, 303, 307, 308]);

module.exports = function createXMLHttpRequest(window) {

const Event = window.Event;
const ProgressEvent = window.ProgressEvent;
const FormData = window.FormData;
const XMLHttpRequestEventTarget = window.XMLHttpRequestEventTarget;
const XMLHttpRequestUpload = window.XMLHttpRequestUpload;

class XMLHttpRequest extends XMLHttpRequestEventTarget {
  constructor() {
    super();
    if (!(this instanceof XMLHttpRequest)) {
      throw new TypeError("DOM object constructor cannot be called as a function.");
    }
    this.upload = new XMLHttpRequestUpload();
    this.upload._ownerDocument = window.document;

    this[xhrSymbols.flag] = {
      synchronous: false,
      withCredentials: false,
      mimeType: null,
      auth: null,
      method: undefined,
      responseType: "",
      requestHeaders: {},
      referrer: this._ownerDocument.URL,
      uri: "",
      timeout: 0,
      body: undefined,
      formData: false,
      preflight: false,
      requestManager: this._ownerDocument._requestManager,
      pool: this._ownerDocument._pool,
      agentOptions: this._ownerDocument._agentOptions,
      strictSSL: this._ownerDocument._strictSSL,
      proxy: this._ownerDocument._proxy,
      cookieJar: this._ownerDocument._cookieJar,
      encoding: this._ownerDocument._encoding,
      origin: this._ownerDocument.origin,
      userAgent: this._ownerDocument._defaultView.navigator.userAgent
    };

    this[xhrSymbols.properties] = {
      beforeSend: false,
      send: false,
      timeoutStart: 0,
      timeoutId: 0,
      timeoutFn: null,
      client: null,
      responseHeaders: {},
      filteredResponseHeaders: [],
      responseBuffer: null,
      responseCache: null,
      responseTextCache: null,
      responseXMLCache: null,
      responseURL: "",
      readyState: XMLHttpRequest.UNSENT,
      status: 0,
      statusText: "",
      error: "",
      uploadComplete: true,
      abortError: false,
      cookieJar: this._ownerDocument._cookieJar
    };
    this.onreadystatechange = null;
  }
  get readyState() {
    return this[xhrSymbols.properties].readyState;
  }
  get status() {
    return this[xhrSymbols.properties].status;
  }
  get statusText() {
    return this[xhrSymbols.properties].statusText;
  }
  get responseType() {
    return this[xhrSymbols.flag].responseType;
  }
  set responseType(responseType) {
    const flag = this[xhrSymbols.flag];
    if (this.readyState === XMLHttpRequest.LOADING || this.readyState === XMLHttpRequest.DONE) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    if (this.readyState === XMLHttpRequest.OPENED && flag.synchronous) {
      throw new DOMException(DOMException.INVALID_ACCESS_ERR);
    }
    if (!XMLHttpRequestResponseType.has(responseType)) {
      responseType = "";
    }
    flag.responseType = responseType;
  }
  get response() {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];
    if (properties.responseCache) {
      return properties.responseCache;
    }
    let res = "";
    switch (this.responseType) {
      case "":
      case "text": {
        res = this.responseText;
        break;
      }
      case "arraybuffer": {
        if (!properties.responseBuffer) {
          return null;
        }
        res = (new Uint8Array(properties.responseBuffer)).buffer;
        break;
      }
      case "blob": {
        if (!properties.responseBuffer) {
          return null;
        }
        const contentType = getContentType(this);
        res = Blob.create([[new Uint8Array(properties.responseBuffer)], {
          type: contentType && contentType.toString() || ""
        }]);
        break;
      }
      case "document": {
        res = this.responseXML;
        break;
      }
      case "json": {
        if (this.readyState !== XMLHttpRequest.DONE || !properties.responseBuffer) {
          res = null;
        }

        const contentType = getContentType(this);
        const fallbackEncoding = whatwgEncoding.labelToName(
          contentType && contentType.get("charset") || flag.encoding);
        const jsonStr = whatwgEncoding.decode(properties.responseBuffer, fallbackEncoding);

        try {
          res = JSON.parse(jsonStr);
        } catch (e) {
          res = null;
        }
        break;
      }
    }
    properties.responseCache = res;
    return res;
  }
  get responseText() {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];
    if (this.responseType !== "" && this.responseType !== "text") {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    if (this.readyState !== XMLHttpRequest.LOADING && this.readyState !== XMLHttpRequest.DONE) {
      return "";
    }
    if (properties.responseTextCache) {
      return properties.responseTextCache;
    }
    const responseBuffer = properties.responseBuffer;
    if (!responseBuffer) {
      return "";
    }

    const contentType = getContentType(this);
    const fallbackEncoding = whatwgEncoding.labelToName(contentType && contentType.get("charset") || flag.encoding);
    const res = whatwgEncoding.decode(responseBuffer, fallbackEncoding);

    properties.responseTextCache = res;
    return res;
  }
  get responseXML() {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];
    if (this.responseType !== "" && this.responseType !== "document") {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    if (this.readyState !== XMLHttpRequest.DONE) {
      return null;
    }
    if (properties.responseXMLCache) {
      return properties.responseXMLCache;
    }
    const responseBuffer = properties.responseBuffer;
    if (!responseBuffer) {
      return null;
    }
    const contentType = getContentType(this);
    let isHTML = false;
    let isXML = false;
    if (contentType) {
      isHTML = contentType.isHTML();
      isXML = contentType.isXML();
      if (!isXML && !isHTML) {
        return null;
      }
    }

    const encoding = whatwgEncoding.getBOMEncoding(responseBuffer) ||
                     whatwgEncoding.labelToName(contentType && contentType.get("charset") || flag.encoding);
    const resText = whatwgEncoding.decode(responseBuffer, encoding);

    if (!resText) {
      return null;
    }
    if (this.responseType === "" && isHTML) {
      return null;
    }
    const res = Document.create([], { core: window._core, options: {
      url: flag.uri,
      lastModified: new Date(getResponseHeader(this, "last-modified")),
      parsingMode: isHTML ? "html" : "xml",
      cookieJar: { setCookieSync: () => undefined, getCookieStringSync: () => "" },
      encoding
    } });
    const resImpl = idlUtils.implForWrapper(res);
    try {
      resImpl._htmlToDom.appendHtmlToDocument(resText, resImpl);
    } catch (e) {
      properties.responseXMLCache = null;
      return null;
    }
    res.close();
    properties.responseXMLCache = res;
    return res;
  }

  get responseURL() {
    return this[xhrSymbols.properties].responseURL;
  }

  get timeout() {
    return this[xhrSymbols.flag].timeout;
  }
  set timeout(val) {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];
    if (flag.synchronous) {
      throw new DOMException(DOMException.INVALID_ACCESS_ERR);
    }
    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[xhrSymbols.flag].withCredentials;
  }
  set withCredentials(val) {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];
    if (!(this.readyState === XMLHttpRequest.UNSENT || this.readyState === XMLHttpRequest.OPENED)) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    if (properties.send) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    flag.withCredentials = val;
  }

  abort() {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];

    // Terminate the request
    clearTimeout(properties.timeoutId);
    properties.timeoutFn = null;
    properties.timeoutStart = 0;

    const client = properties.client;
    if (client) {
      client.abort();
      properties.client = null;
    }

    if ((this.readyState === XMLHttpRequest.OPENED && properties.send) ||
        this.readyState === XMLHttpRequest.HEADERS_RECEIVED ||
        this.readyState === XMLHttpRequest.LOADING) {
      // Run the request error steps for event abort
      properties.readyState = XMLHttpRequest.DONE;
      properties.send = false;

      properties.status = 0;
      properties.statusText = "";
      properties.responseCache = properties.responseTextCache = properties.responseXMLCache = null;

      if (flag.synchronous) {
        throw new DOMException(DOMException.ABORT_ERR);
      }

      this.dispatchEvent(new Event("readystatechange"));

      // TODO: spec says this should only be checking upload complete flag?
      if (!(flag.method === "HEAD" || flag.method === "GET")) {
        properties.uploadComplete = true;

        // TODO upload listener

        this.upload.dispatchEvent(new ProgressEvent("abort"));
        if (properties.abortError) {
          // TODO document what this is about (here and below)
          this.upload.dispatchEvent(new ProgressEvent("error"));
        }
        this.upload.dispatchEvent(new ProgressEvent("loadend"));
      }

      this.dispatchEvent(new ProgressEvent("abort"));
      if (properties.abortError) {
        this.dispatchEvent(new ProgressEvent("error"));
      }
      this.dispatchEvent(new ProgressEvent("loadend"));
    }

    if (this.readyState === XMLHttpRequest.DONE) {
      properties.readyState = XMLHttpRequest.UNSENT;

      properties.status = 0;
      properties.statusText = "";
      properties.responseCache = properties.responseTextCache = properties.responseXMLCache = null;
    }
  }
  getAllResponseHeaders() {
    const properties = this[xhrSymbols.properties];
    const readyState = this.readyState;
    if (readyState === XMLHttpRequest.UNSENT || readyState === XMLHttpRequest.OPENED) {
      return "";
    }
    return Object.keys(properties.responseHeaders)
      .filter(key => properties.filteredResponseHeaders.indexOf(key) === -1)
      .map(key => [key, properties.responseHeaders[key]].join(": ")).join("\r\n");
  }

  getResponseHeader(header) {
    const properties = this[xhrSymbols.properties];
    const readyState = this.readyState;
    if (readyState === XMLHttpRequest.UNSENT || readyState === XMLHttpRequest.OPENED) {
      return null;
    }
    const lcHeader = toByteString(header).toLowerCase();
    if (properties.filteredResponseHeaders.find(filtered => lcHeader === filtered.toLowerCase())) {
      return null;
    }
    return getResponseHeader(this, lcHeader);
  }

  open(method, uri, asynchronous, user, password) {
    if (!this._ownerDocument) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];
    const argumentCount = arguments.length;
    if (argumentCount < 2) {
      throw new TypeError("Not enought arguments");
    }
    method = toByteString(method);
    if (!tokenRegexp.test(method)) {
      throw new DOMException(DOMException.SYNTAX_ERR);
    }
    const upperCaseMethod = method.toUpperCase();
    if (forbiddenRequestMethods.has(upperCaseMethod)) {
      throw new DOMException(DOMException.SECURITY_ERR);
    }

    const client = properties.client;
    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 new DOMException(DOMException.INVALID_ACCESS_ERR);
    }
    if (flag.synchronous && flag.timeout) {
      throw new DOMException(DOMException.INVALID_ACCESS_ERR);
    }
    flag.method = method;

    let urlObj;
    try {
      urlObj = new URL(uri, documentBaseURLSerialized(this._ownerDocument));
    } catch (e) {
      throw new DOMException(DOMException.SYNTAX_ERR);
    }

    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.requestBuffer = null;
    properties.requestCache = null;
    properties.abortError = false;
    properties.responseURL = "";
    readyStateChange(this, XMLHttpRequest.OPENED);
  }

  overrideMimeType(mime) {
    const readyState = this.readyState;
    if (readyState === XMLHttpRequest.LOADING || readyState === XMLHttpRequest.DONE) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    if (!mime) {
      throw new DOMException(DOMException.SYNTAX_ERR);
    }
    mime = String(mime);
    if (!parseContentType(mime)) {
      throw new DOMException(DOMException.SYNTAX_ERR);
    }
    this[xhrSymbols.flag].mimeType = mime;
  }

  send(body) {
    if (!this._ownerDocument) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];

    if (this.readyState !== XMLHttpRequest.OPENED || properties.send) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }

    properties.beforeSend = true;

    try {
      if (!flag.body &&
          body !== undefined &&
          body !== null &&
          body !== "" &&
          !(flag.method === "HEAD" || flag.method === "GET")) {
        let contentType = null;
        let encoding = null;
        if (body instanceof FormData) {
          flag.formData = true;
          const formData = [];
          for (const entry of idlUtils.implForWrapper(body)._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);
          }
          flag.body = formData;
          // TODO content type; what is the form boundary?
        } else if (Blob.is(body)) {
          const blob = idlUtils.implForWrapper(body);
          flag.body = blob._buffer;
          if (blob.type !== "") {
            contentType = blob.type;
          }
        } else if (body instanceof ArrayBuffer) {
          flag.body = new Buffer(new Uint8Array(body));
        } else if (body instanceof Document.interface) {
          if (body.childNodes.length === 0) {
            throw new DOMException(DOMException.INVALID_STATE_ERR);
          }
          flag.body = domToHtml([body]);

          encoding = "UTF-8";

          const documentBodyParsingMode = idlUtils.implForWrapper(body)._parsingMode;
          contentType = documentBodyParsingMode === "html" ? "text/html" : "application/xml";
          contentType += ";charset=UTF-8";
        } else if (typeof body !== "string") {
          flag.body = String(body);
        } else {
          flag.body = body;
          contentType = "text/plain;charset=UTF-8";
          encoding = "UTF-8";
        }

        const existingContentType = xhrUtils.getRequestHeader(flag.requestHeaders, "content-type");
        if (contentType !== null && existingContentType === null) {
          flag.requestHeaders["Content-Type"] = contentType;
        } else if (existingContentType !== null && encoding !== null) {
          const parsed = parseContentType(existingContentType);
          if (parsed) {
            parsed.parameterList
              .filter(v => v.key && v.key.toLowerCase() === "charset" &&
                      whatwgEncoding.labelToName(v.value) !== "UTF-8")
              .forEach(v => {
                v.value = "UTF-8";
              });
            xhrUtils.updateRequestHeader(flag.requestHeaders, "content-type", parsed.toString());
          }
        }
      }
    } finally {
      if (properties.beforeSend) {
        properties.beforeSend = false;
      } else {
        throw new DOMException(DOMException.INVALID_STATE_ERR);
      }
    }

    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 }
      );
      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(), (k, v) => {
        if (k === "responseBuffer" && v && v.data) {
          return new Buffer(v.data);
        }
        if (k === "cookieJar" && v) {
          return tough.CookieJar.deserializeSync(v, this._ownerDocument._cookieJar.store);
        }
        return v;
      });
      response.properties.readyState = XMLHttpRequest.LOADING;
      this[xhrSymbols.properties] = response.properties;

      if (response.properties.error) {
        dispatchError(this);
        throw new DOMException(DOMException.NETWORK_ERR, response.properties.error);
      } else {
        const responseBuffer = this[xhrSymbols.properties].responseBuffer;
        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;
        }
        this.dispatchEvent(new ProgressEvent("progress", progressObj));
        readyStateChange(this, XMLHttpRequest.DONE);
        this.dispatchEvent(new ProgressEvent("load", progressObj));
        this.dispatchEvent(new ProgressEvent("loadend", progressObj));
      }
    } else {
      properties.send = true;

      this.dispatchEvent(new ProgressEvent("loadstart"));

      const client = xhrUtils.createClient(this);

      properties.client = client;

      properties.origin = flag.origin;

      client.on("error", err => {
        client.removeAllListeners();
        properties.error = err;
        dispatchError(this);
      });

      client.on("response", res => receiveResponse(this, res));

      client.on("redirect", () => {
        if (flag.preflight) {
          properties.error = "Redirect after preflight forbidden";
          dispatchError(this);
          client.abort();
          return;
        }

        const response = client.response;
        const destUrlObj = new URL(response.request.headers.Referer);

        const urlObj = new URL(response.request.uri.href);

        if (destUrlObj.origin !== urlObj.origin && destUrlObj.origin !== flag.origin) {
          properties.origin = "null";
        }

        response.request.headers.Origin = properties.origin;

        if (flag.origin !== destUrlObj.origin &&
            destUrlObj.protocol !== "data:") {
          if (!validCORSHeaders(this, response, flag, properties, flag.origin)) {
            return;
          }
          if (urlObj.username || urlObj.password || response.request.uri.href.match(/^https?:\/\/:@/)) {
            properties.error = "Userinfo forbidden in cors redirect";
            dispatchError(this);
            return;
          }
        }
      });
      if (body !== undefined &&
        body !== null &&
        body !== "" &&
        !(flag.method === "HEAD" || flag.method === "GET")) {
        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 === XMLHttpRequest.UNSENT ||
              (this.readyState === XMLHttpRequest.OPENED && !properties.send) ||
              this.readyState === XMLHttpRequest.DONE)) {
            properties.send = false;
            let stateChanged = false;
            if (!(flag.method === "HEAD" || flag.method === "GET")) {
              this.upload.dispatchEvent(new ProgressEvent("progress"));
              readyStateChange(this, XMLHttpRequest.DONE);
              this.upload.dispatchEvent(new ProgressEvent("timeout"));
              this.upload.dispatchEvent(new ProgressEvent("loadend"));
              stateChanged = true;
            }
            this.dispatchEvent(new ProgressEvent("progress"));
            if (!stateChanged) {
              readyStateChange(this, XMLHttpRequest.DONE);
            }
            this.dispatchEvent(new ProgressEvent("timeout"));
            this.dispatchEvent(new ProgressEvent("loadend"));
          }
          properties.readyState = XMLHttpRequest.UNSENT;
        };
        properties.timeoutId = setTimeout(properties.timeoutFn, this.timeout);
      }
    }
    flag.body = undefined;
    flag.formData = false;
  }

  setRequestHeader(header, value) {
    const flag = this[xhrSymbols.flag];
    const properties = this[xhrSymbols.properties];

    if (arguments.length !== 2) {
      throw new TypeError("2 arguments required for setRequestHeader");
    }
    header = toByteString(header);
    value = toByteString(value);

    if (this.readyState !== XMLHttpRequest.OPENED || properties.send) {
      throw new DOMException(DOMException.INVALID_STATE_ERR);
    }

    value = normalizeHeaderValue(value);

    if (!tokenRegexp.test(header) || !fieldValueRegexp.test(value)) {
      throw new DOMException(DOMException.SYNTAX_ERR);
    }

    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[lcHeader] = value;
  }

  toString() {
    return "[object XMLHttpRequest]";
  }

  get _ownerDocument() {
    return idlUtils.implForWrapper(window.document);
  }
}

addConstants(XMLHttpRequest, {
  UNSENT: 0,
  OPENED: 1,
  HEADERS_RECEIVED: 2,
  LOADING: 3,
  DONE: 4
});

function readyStateChange(xhr, readyState) {
  const properties = xhr[xhrSymbols.properties];
  if (properties.readyState === readyState) {
    return;
  }

  properties.readyState = readyState;

  const readyStateChangeEvent = new Event("readystatechange");
  xhr.dispatchEvent(readyStateChangeEvent);
}

function receiveResponse(xhr, response) {
  const properties = xhr[xhrSymbols.properties];
  const flag = xhr[xhrSymbols.flag];

  const statusCode = response.statusCode;

  if (flag.preflight && redirectStatuses.has(statusCode)) {
    properties.error = "Redirect after preflight forbidden";
    dispatchError(this);
    return;
  }

  let byteOffset = 0;

  const headers = {};
  const filteredResponseHeaders = [];
  const headerMap = {};
  const rawHeaders = response.rawHeaders;
  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(response.request.uri.href);
  if (properties.origin !== destUrlObj.origin &&
      destUrlObj.protocol !== "data:") {
    if (!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(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);
    }
  }

  properties.responseURL = destUrlObj.href;

  properties.status = statusCode;
  properties.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;
  }
  properties.responseBuffer = new Buffer(0);
  properties.responseCache = null;
  properties.responseTextCache = null;
  properties.responseXMLCache = null;
  readyStateChange(xhr, XMLHttpRequest.HEADERS_RECEIVED);

  // 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.responseBuffer = Buffer.concat([properties.responseBuffer, chunk]);
    properties.responseCache = null;
    properties.responseTextCache = null;
    properties.responseXMLCache = null;

    if (properties.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
      properties.readyState = XMLHttpRequest.LOADING;
    }
    xhr.dispatchEvent(new Event("readystatechange"));

    if (progressObj.total !== progressObj.loaded || properties.responseBuffer.length === 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;
        xhr.dispatchEvent(new ProgressEvent("progress", progressObj));
      }
    }
  });
  properties.client.on("end", () => {
    clearTimeout(properties.timeoutId);
    properties.timeoutFn = null;
    properties.timeoutStart = 0;
    properties.client = null;
    xhr.dispatchEvent(new ProgressEvent("progress", progressObj));
    readyStateChange(xhr, XMLHttpRequest.DONE);
    xhr.dispatchEvent(new ProgressEvent("load", progressObj));
    xhr.dispatchEvent(new ProgressEvent("loadend", progressObj));
  });
}

function setDispatchProgressEvents(xhr) {
  const properties = xhr[xhrSymbols.properties];
  const client = properties.client;
  const upload = xhr.upload;

  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
  };
  upload.dispatchEvent(new ProgressEvent("loadstart", initProgress));

  client.on("request", req => {
    req.on("response", () => {
      properties.uploadComplete = true;
      const progress = {
        lengthComputable,
        total,
        loaded: total
      };
      upload.dispatchEvent(new ProgressEvent("progress", progress));
      upload.dispatchEvent(new ProgressEvent("load", progress));
      upload.dispatchEvent(new ProgressEvent("loadend", progress));
    });
  });
}

function dispatchError(xhr) {
  const properties = xhr[xhrSymbols.properties];
  readyStateChange(xhr, XMLHttpRequest.DONE);
  if (!properties.uploadComplete) {
    xhr.upload.dispatchEvent(new ProgressEvent("error"));
    xhr.upload.dispatchEvent(new ProgressEvent("loadend"));
  }
  xhr.dispatchEvent(new ProgressEvent("error"));
  xhr.dispatchEvent(new ProgressEvent("loadend"));
  if (xhr._ownerDocument) {
    const error = new Error(properties.error);
    error.type = "XMLHttpRequest";

    xhr._ownerDocument._defaultView._virtualConsole.emit("jsdomError", error);
  }
}

function validCORSHeaders(xhr, response, flag, properties, origin) {
  const acaoStr = response.headers["access-control-allow-origin"];
  const acao = acaoStr ? acaoStr.trim() : null;
  if (acao !== "*" && acao !== origin) {
    properties.error = "Cross origin " + origin + " forbidden";
    dispatchError(xhr);
    return false;
  }
  const acacStr = response.headers["access-control-allow-credentials"];
  const acac = acacStr ? acacStr.trim() : null;
  if (flag.withCredentials && acac !== "true") {
    properties.error = "Credentials forbidden";
    dispatchError(xhr);
    return false;
  }
  const acahStr = response.headers["access-control-allow-headers"];
  const acah = new Set(acahStr ? acahStr.trim().toLowerCase().split(headerListSeparatorRegexp) : []);
  const forbiddenHeaders = Object.keys(flag.requestHeaders).filter(header => {
    const lcHeader = header.toLowerCase();
    return !simpleHeaders.has(lcHeader) && !acah.has(lcHeader);
  });
  if (forbiddenHeaders.length > 0) {
    properties.error = "Headers " + forbiddenHeaders + " forbidden";
    dispatchError(xhr);
    return false;
  }
  return true;
}

function toByteString(value) {
  value = String(value);
  if (!/^[\0-\xFF]*$/.test(value)) {
    throw new TypeError("invalid ByteString");
  }
  return value;
}

function getContentType(xhr) {
  const flag = xhr[xhrSymbols.flag];
  return parseContentType(flag.mimeType || getResponseHeader(xhr, "content-type"));
}

function getResponseHeader(xhr, lcHeader) {
  const properties = xhr[xhrSymbols.properties];
  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]+$/, "");
}

return XMLHttpRequest;

};