“use strict”;

const parseContentType = require(“content-type-parser”); const sniffHTMLEncoding = require(“html-encoding-sniffer”); const whatwgEncoding = require(“whatwg-encoding”); const parseDataUrl = require(“../utils”).parseDataUrl; const fs = require(“fs”); const request = require(“request”); const documentBaseURLSerialized = require(“../living/helpers/document-base-url”).documentBaseURLSerialized; const NODE_TYPE = require(“../living/node-type”);

/* eslint-disable no-restricted-modules */ // TODO: stop using the built-in URL in favor of the spec-compliant whatwg-url package // This legacy usage is in the process of being purged. const URL = require(“url”); /* eslint-enable no-restricted-modules */

const IS_BROWSER = Object.prototype.toString.call(process) !== “[object process]”;

function createResourceLoadHandler(element, resourceUrl, document, loadCallback) {

if (loadCallback === undefined) {
  loadCallback = () => {
    // do nothing
  };
}
return (err, data, response) => {
  const ev = document.createEvent("HTMLEvents");

  if (!err) {
    try {
      loadCallback.call(element, data, resourceUrl, response);
      ev.initEvent("load", false, false);
    } catch (e) {
      err = e;
    }
  }

  if (err) {
    if (!err.isAbortError) {
      ev.initEvent("error", false, false);
      ev.error = err;
      element.dispatchEvent(ev);

      const error = new Error(`Could not load ${element.localName}: "${resourceUrl}"`);
      error.detail = err;
      error.type = "resource loading";

      document._defaultView._virtualConsole.emit("jsdomError", error);
    }
  } else {
    element.dispatchEvent(ev);
  }
};

}

exports.readFile = function (filePath, options, callback) {

const readableStream = fs.createReadStream(filePath);

let data = new Buffer(0);

readableStream.on("error", callback);

readableStream.on("data", chunk => {
  data = Buffer.concat([data, chunk]);
});

const defaultEncoding = options.defaultEncoding;
const detectMetaCharset = options.detectMetaCharset;

readableStream.on("end", () => {
  // Not passing default encoding means binary
  if (defaultEncoding) {
    const encoding = detectMetaCharset ? sniffHTMLEncoding(data, { defaultEncoding }) :
                     whatwgEncoding.getBOMEncoding(data) || defaultEncoding;
    const decoded = whatwgEncoding.decode(data, encoding);
    callback(null, decoded, { headers: { "content-type": "text/plain;charset=" + encoding } });
  } else {
    callback(null, data);
  }
});

return {
  abort() {
    readableStream.destroy();
    const error = new Error("request canceled by user");
    error.isAbortError = true;
    callback(error);
  }
};

};

function readDataUrl(dataUrl, options, callback) {

const defaultEncoding = options.defaultEncoding;
try {
  const data = parseDataUrl(dataUrl);
  // If default encoding does not exist, pass on binary data.
  if (defaultEncoding) {
    const contentType = parseContentType(data.type) || parseContentType("text/plain");
    const sniffOptions = {
      transportLayerEncodingLabel: contentType.get("charset"),
      defaultEncoding
    };

    const encoding = options.detectMetaCharset ? sniffHTMLEncoding(data.buffer, sniffOptions) :
                     whatwgEncoding.getBOMEncoding(data.buffer) ||
                     whatwgEncoding.labelToName(contentType.get("charset")) || defaultEncoding;
    const decoded = whatwgEncoding.decode(data.buffer, encoding);

    contentType.set("charset", encoding);
    data.type = contentType.toString();

    callback(null, decoded, { headers: { "content-type": data.type } });
  } else {
    callback(null, data.buffer, { headers: { "content-type": data.type } });
  }
} catch (err) {
  callback(err, null);
}
return null;

}

// NOTE: request wraps tough-cookie cookie jar // (see: github.com/request/request/blob/master/lib/cookies.js). // Therefore, to pass our cookie jar to the request, we need to create // request's wrapper and monkey patch it with our jar. function wrapCookieJarForRequest(cookieJar) {

const jarWrapper = request.jar();
jarWrapper._jar = cookieJar;
return jarWrapper;

}

function fetch(urlObj, options, callback) {

if (urlObj.protocol === "data:") {
  return readDataUrl(urlObj.href, options, callback);
} else if (urlObj.hostname) {
  return exports.download(urlObj, options, callback);
}
const filePath = urlObj.pathname
  .replace(/^file:\/\//, "")
  .replace(/^\/([a-z]):\//i, "$1:/")
  .replace(/%20/g, " ");
return exports.readFile(filePath, options, callback);

}

exports.enqueue = function (element, resourceUrl, callback) {

const document = element.nodeType === NODE_TYPE.DOCUMENT_NODE ? element : element._ownerDocument;

if (document._queue) {
  const loadHandler = createResourceLoadHandler(element, resourceUrl || document.URL, document, callback);
  return document._queue.push(loadHandler);
}

return () => {
  // do nothing in queue-less documents
};

};

exports.download = function (url, options, callback) {

const requestOptions = {
  pool: options.pool,
  agent: options.agent,
  agentOptions: options.agentOptions,
  agentClass: options.agentClass,
  strictSSL: options.strictSSL,
  gzip: true,
  jar: wrapCookieJarForRequest(options.cookieJar),
  encoding: null,
  headers: {
    "User-Agent": options.userAgent,
    "Accept-Language": "en",
    Accept: options.accept || "*/*"
  }
};
if (options.referrer && !IS_BROWSER) {
  requestOptions.headers.referer = options.referrer;
}
if (options.proxy) {
  requestOptions.proxy = options.proxy;
}
Object.assign(requestOptions.headers, options.headers);

const defaultEncoding = options.defaultEncoding;
const detectMetaCharset = options.detectMetaCharset;

const req = request(url, requestOptions, (error, response, bufferData) => {
  if (!error) {
    // If default encoding does not exist, pass on binary data.
    if (defaultEncoding) {
      const contentType = parseContentType(response.headers["content-type"]) || parseContentType("text/plain");
      const sniffOptions = {
        transportLayerEncodingLabel: contentType.get("charset"),
        defaultEncoding
      };

      const encoding = detectMetaCharset ? sniffHTMLEncoding(bufferData, sniffOptions) :
                      whatwgEncoding.getBOMEncoding(bufferData) ||
                      whatwgEncoding.labelToName(contentType.get("charset")) || defaultEncoding;
      const decoded = whatwgEncoding.decode(bufferData, encoding);

      contentType.set("charset", encoding);
      response.headers["content-type"] = contentType.toString();

      callback(null, decoded, response);
    } else {
      callback(null, bufferData, response);
    }
  } else {
    callback(error, null, response);
  }
});
return {
  abort() {
    req.abort();
    const error = new Error("request canceled by user");
    error.isAbortError = true;
    callback(error);
  }
};

};

exports.load = function (element, urlString, options, callback) {

const document = element._ownerDocument;
const documentImpl = document.implementation;

if (!documentImpl._hasFeature("FetchExternalResources", element.tagName.toLowerCase())) {
  return;
}

if (documentImpl._hasFeature("SkipExternalResources", urlString)) {
  return;
}

const urlObj = URL.parse(urlString);
const enqueued = exports.enqueue(element, urlString, callback);
const customLoader = document._customResourceLoader;
const requestManager = document._requestManager;
const cookieJar = document._cookieJar;

options.accept = element._accept;
options.cookieJar = cookieJar;
options.referrer = document.URL;
options.pool = document._pool;
options.agentOptions = document._agentOptions;
options.strictSSL = document._strictSSL;
options.proxy = document._proxy;
options.userAgent = document._defaultView.navigator.userAgent;

let req = null;
function wrappedEnqueued() {
  if (req && requestManager) {
    requestManager.remove(req);
  }
  // do not trigger if the window is closed
  if (element._ownerDocument && element._ownerDocument.defaultView.document) {
    enqueued.apply(this, arguments);
  }
}
if (typeof customLoader === "function") {
  req = customLoader({
    element,
    url: urlObj,
    cookie: cookieJar.getCookieStringSync(urlObj, { http: true }),
    baseUrl: documentBaseURLSerialized(document),
    defaultFetch(fetchCallback) {
      return fetch(urlObj, options, fetchCallback);
    }
  },
  wrappedEnqueued);
} else {
  req = fetch(urlObj, options, wrappedEnqueued);
}
if (req && requestManager) {
  requestManager.add(req);
}

};