You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							503 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
	
	
							503 lines
						
					
					
						
							15 KiB
						
					
					
				"use strict"; | 
						|
/* eslint-disable no-unused-expressions */ | 
						|
() => `jsdom 7.x onward only works on Node.js 4 or newer: https://github.com/tmpvar/jsdom#install`; | 
						|
/* eslint-enable no-unused-expressions */ | 
						|
 | 
						|
const fs = require("fs"); | 
						|
const path = require("path"); | 
						|
const { CookieJar } = require("tough-cookie"); | 
						|
const MIMEType = require("whatwg-mimetype"); | 
						|
 | 
						|
const { toFileUrl } = require("./jsdom/utils"); | 
						|
const documentFeatures = require("./jsdom/browser/documentfeatures"); | 
						|
const { domToHtml } = require("./jsdom/browser/domtohtml"); | 
						|
const Window = require("./jsdom/browser/Window"); | 
						|
const resourceLoader = require("./jsdom/browser/resource-loader"); | 
						|
const VirtualConsole = require("./jsdom/virtual-console"); | 
						|
const idlUtils = require("./jsdom/living/generated/utils"); | 
						|
const Blob = require("./jsdom/living/generated/Blob"); | 
						|
 | 
						|
const whatwgURL = require("whatwg-url"); | 
						|
 | 
						|
require("./jsdom/living"); // Enable living standard features | 
						|
 | 
						|
/* 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 canReadFilesFromFS = Boolean(fs.readFile); // in a browserify environment, this isn't present | 
						|
 | 
						|
exports.createVirtualConsole = function (options) { | 
						|
  return new VirtualConsole(options); | 
						|
}; | 
						|
 | 
						|
exports.getVirtualConsole = function (window) { | 
						|
  return window._virtualConsole; | 
						|
}; | 
						|
 | 
						|
exports.createCookieJar = function () { | 
						|
  return new CookieJar(null, { looseMode: true }); | 
						|
}; | 
						|
 | 
						|
exports.nodeLocation = function (node) { | 
						|
  return idlUtils.implForWrapper(node).__location; | 
						|
}; | 
						|
 | 
						|
exports.reconfigureWindow = function (window, newProps) { | 
						|
  if ("top" in newProps) { | 
						|
    window._top = newProps.top; | 
						|
  } | 
						|
}; | 
						|
 | 
						|
exports.changeURL = function (window, urlString) { | 
						|
  const doc = idlUtils.implForWrapper(window._document); | 
						|
 | 
						|
  const url = whatwgURL.parseURL(urlString); | 
						|
 | 
						|
  if (url === null) { | 
						|
    throw new TypeError(`Could not parse "${urlString}" as a URL`); | 
						|
  } | 
						|
 | 
						|
  doc._URL = url; | 
						|
  doc.origin = whatwgURL.serializeURLOrigin(doc._URL); | 
						|
}; | 
						|
 | 
						|
// Proxy to features module | 
						|
Object.defineProperty(exports, "defaultDocumentFeatures", { | 
						|
  enumerable: true, | 
						|
  configurable: true, | 
						|
  get() { | 
						|
    return documentFeatures.defaultDocumentFeatures; | 
						|
  }, | 
						|
  set(v) { | 
						|
    documentFeatures.defaultDocumentFeatures = v; | 
						|
  } | 
						|
}); | 
						|
 | 
						|
exports.jsdom = function (html, options) { | 
						|
  if (options === undefined) { | 
						|
    options = {}; | 
						|
  } | 
						|
  if (options.parsingMode === undefined || options.parsingMode === "auto") { | 
						|
    options.parsingMode = "html"; | 
						|
  } | 
						|
 | 
						|
  if (options.parsingMode !== "html" && options.parsingMode !== "xml") { | 
						|
    throw new RangeError(`Invalid parsingMode option ${JSON.stringify(options.parsingMode)}; must be either "html", ` + | 
						|
      `"xml", "auto", or undefined`); | 
						|
  } | 
						|
 | 
						|
  options.encoding = options.encoding || "UTF-8"; | 
						|
 | 
						|
  setGlobalDefaultConfig(options); | 
						|
 | 
						|
  // Back-compat hack: we have previously suggested nesting these under document, for jsdom.env at least. | 
						|
  // So we need to support that. | 
						|
  if (options.document) { | 
						|
    if (options.document.cookie !== undefined) { | 
						|
      options.cookie = options.document.cookie; | 
						|
    } | 
						|
    if (options.document.referrer !== undefined) { | 
						|
      options.referrer = options.document.referrer; | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  // Adapt old API `features: { ProcessExternalResources: ["script"] }` to the runScripts option. | 
						|
  // This is part of a larger effort to eventually remove the document features infrastructure entirely. It's unclear | 
						|
  // whether we'll kill the old API or document features first, but as long as old API survives, attempts to kill | 
						|
  // document features will need this kind of adapter. | 
						|
  if (!options.features) { | 
						|
    options.features = exports.defaultDocumentFeatures; | 
						|
  } | 
						|
  if (options.features.ProcessExternalResources === undefined) { | 
						|
    options.features.ProcessExternalResources = ["script"]; | 
						|
  } | 
						|
  const ProcessExternalResources = options.features.ProcessExternalResources || []; | 
						|
  if (ProcessExternalResources === "script" || | 
						|
      (ProcessExternalResources.includes && ProcessExternalResources.includes("script"))) { | 
						|
    options.runScripts = "dangerously"; | 
						|
  } | 
						|
 | 
						|
  if (options.pretendToBeVisual !== undefined) { | 
						|
    options.pretendToBeVisual = Boolean(options.pretendToBeVisual); | 
						|
  } else { | 
						|
    options.pretendToBeVisual = false; | 
						|
  } | 
						|
 | 
						|
  options.storageQuota = options.storageQuota || 5000000; | 
						|
 | 
						|
  // List options explicitly to be clear which are passed through | 
						|
  const window = new Window({ | 
						|
    parsingMode: options.parsingMode, | 
						|
    parseOptions: options.parseOptions, | 
						|
    contentType: options.contentType, | 
						|
    encoding: options.encoding, | 
						|
    url: options.url, | 
						|
    lastModified: options.lastModified, | 
						|
    referrer: options.referrer, | 
						|
    cookieJar: options.cookieJar, | 
						|
    cookie: options.cookie, | 
						|
    resourceLoader: options.resourceLoader, | 
						|
    deferClose: options.deferClose, | 
						|
    concurrentNodeIterators: options.concurrentNodeIterators, | 
						|
    virtualConsole: options.virtualConsole, | 
						|
    pool: options.pool, | 
						|
    agent: options.agent, | 
						|
    agentClass: options.agentClass, | 
						|
    agentOptions: options.agentOptions, | 
						|
    strictSSL: options.strictSSL, | 
						|
    proxy: options.proxy, | 
						|
    userAgent: options.userAgent, | 
						|
    runScripts: options.runScripts, | 
						|
    pretendToBeVisual: options.pretendToBeVisual, | 
						|
    storageQuota: options.storageQuota | 
						|
  }); | 
						|
 | 
						|
  const documentImpl = idlUtils.implForWrapper(window.document); | 
						|
  documentFeatures.applyDocumentFeatures(documentImpl, options.features); | 
						|
 | 
						|
  if (options.created) { | 
						|
    options.created(null, window.document.defaultView); | 
						|
  } | 
						|
 | 
						|
  if (options.parsingMode === "html") { | 
						|
    if (html === undefined || html === "") { | 
						|
      html = "<html><head></head><body></body></html>"; | 
						|
    } | 
						|
 | 
						|
    window.document.write(html); | 
						|
  } else if (options.parsingMode === "xml") { | 
						|
    if (html !== undefined) { | 
						|
      documentImpl._htmlToDom.appendToDocument(html, documentImpl); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  if (window.document.close && !options.deferClose) { | 
						|
    window.document.close(); | 
						|
  } | 
						|
 | 
						|
  return window.document; | 
						|
}; | 
						|
 | 
						|
exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) { | 
						|
  if (!window || !window.document) { | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  const implImpl = idlUtils.implForWrapper(window.document.implementation); | 
						|
  const oldFeatures = implImpl._features; | 
						|
  const oldRunScripts = window._runScripts; | 
						|
 | 
						|
  implImpl._addFeature("FetchExternalResources", ["script"]); | 
						|
  documentFeatures.contextifyWindow(idlUtils.implForWrapper(window.document)._global); | 
						|
  window._runScripts = "dangerously"; | 
						|
 | 
						|
  const scriptEl = window.document.createElement("script"); | 
						|
  scriptEl.className = "jsdom"; | 
						|
  scriptEl.src = jqueryUrl; | 
						|
  scriptEl.onload = scriptEl.onerror = () => { | 
						|
    implImpl._features = oldFeatures; | 
						|
    window._runScripts = oldRunScripts; | 
						|
    // Can't un-contextify the window. Oh well. That's what we get for such magic behavior in old API. | 
						|
 | 
						|
    if (callback) { | 
						|
      callback(window, window.jQuery); | 
						|
    } | 
						|
  }; | 
						|
 | 
						|
  window.document.body.appendChild(scriptEl); | 
						|
}; | 
						|
 | 
						|
exports.env = exports.jsdom.env = function () { | 
						|
  const config = getConfigFromEnvArguments(arguments); | 
						|
  let req = null; | 
						|
 | 
						|
  if (config.file && canReadFilesFromFS) { | 
						|
    req = resourceLoader.readFile( | 
						|
      config.file, | 
						|
      { defaultEncoding: config.defaultEncoding, detectMetaCharset: true }, | 
						|
      (err, text, res) => { | 
						|
        if (err) { | 
						|
          reportInitError(err, config); | 
						|
          return; | 
						|
        } | 
						|
 | 
						|
        const contentType = new MIMEType(res.headers["content-type"]); | 
						|
        config.encoding = contentType.parameters.get("charset"); | 
						|
        setParsingModeFromExtension(config, config.file); | 
						|
 | 
						|
        config.html = text; | 
						|
        processHTML(config); | 
						|
      } | 
						|
    ); | 
						|
  } else if (config.html !== undefined) { | 
						|
    processHTML(config); | 
						|
  } else if (config.url) { | 
						|
    req = handleUrl(config); | 
						|
  } else if (config.somethingToAutodetect !== undefined) { | 
						|
    const url = URL.parse(config.somethingToAutodetect); | 
						|
    if (url.protocol && url.hostname) { | 
						|
      config.url = config.somethingToAutodetect; | 
						|
      req = handleUrl(config.somethingToAutodetect); | 
						|
    } else if (canReadFilesFromFS) { | 
						|
      try { | 
						|
        req = resourceLoader.readFile( | 
						|
          config.somethingToAutodetect, | 
						|
          { defaultEncoding: config.defaultEncoding, detectMetaCharset: true }, | 
						|
          (err, text, res) => { | 
						|
            if (err) { | 
						|
              if (err.code === "ENOENT" || err.code === "ENAMETOOLONG" || err.code === "ERR_INVALID_ARG_TYPE") { | 
						|
                config.html = config.somethingToAutodetect; | 
						|
                processHTML(config); | 
						|
              } else { | 
						|
                reportInitError(err, config); | 
						|
              } | 
						|
            } else { | 
						|
              const contentType = new MIMEType(res.headers["content-type"]); | 
						|
              config.encoding = contentType.parameters.get("charset"); | 
						|
              setParsingModeFromExtension(config, config.somethingToAutodetect); | 
						|
 | 
						|
              config.html = text; | 
						|
              config.url = toFileUrl(config.somethingToAutodetect); | 
						|
              processHTML(config); | 
						|
            } | 
						|
          } | 
						|
        ); | 
						|
      } catch (err) { | 
						|
        config.html = config.somethingToAutodetect; | 
						|
        processHTML(config); | 
						|
      } | 
						|
    } else { | 
						|
      config.html = config.somethingToAutodetect; | 
						|
      processHTML(config); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  function handleUrl() { | 
						|
    config.cookieJar = config.cookieJar || exports.createCookieJar(); | 
						|
 | 
						|
    const options = { | 
						|
      defaultEncoding: config.defaultEncoding, | 
						|
      detectMetaCharset: true, | 
						|
      headers: config.headers, | 
						|
      pool: config.pool, | 
						|
      strictSSL: config.strictSSL, | 
						|
      proxy: config.proxy, | 
						|
      cookieJar: config.cookieJar, | 
						|
      userAgent: config.userAgent, | 
						|
      agent: config.agent, | 
						|
      agentClass: config.agentClass, | 
						|
      agentOptions: config.agentOptions | 
						|
    }; | 
						|
 | 
						|
    const { fragment } = whatwgURL.parseURL(config.url); | 
						|
 | 
						|
    return resourceLoader.download(config.url, options, (err, responseText, res) => { | 
						|
      if (err) { | 
						|
        reportInitError(err, config); | 
						|
        return; | 
						|
      } | 
						|
 | 
						|
      // The use of `res.request.uri.href` ensures that `window.location.href` | 
						|
      // is updated when `request` follows redirects. | 
						|
      config.html = responseText; | 
						|
      config.url = res.request.uri.href; | 
						|
      if (fragment) { | 
						|
        config.url += `#${fragment}`; | 
						|
      } | 
						|
 | 
						|
      if (res.headers["last-modified"]) { | 
						|
        config.lastModified = new Date(res.headers["last-modified"]); | 
						|
      } | 
						|
 | 
						|
      const contentType = new MIMEType(res.headers["content-type"]); | 
						|
      if (config.parsingMode === "auto") { | 
						|
        if (contentType.isXML()) { | 
						|
          config.parsingMode = "xml"; | 
						|
        } | 
						|
      } | 
						|
      config.contentType = contentType.essence; | 
						|
      config.encoding = contentType.parameters.get("charset"); | 
						|
 | 
						|
      processHTML(config); | 
						|
    }); | 
						|
  } | 
						|
  return req; | 
						|
}; | 
						|
 | 
						|
exports.serializeDocument = function (doc) { | 
						|
  return domToHtml([idlUtils.implForWrapper(doc)]); | 
						|
}; | 
						|
 | 
						|
exports.blobToBuffer = function (blob) { | 
						|
  return (Blob.is(blob) && idlUtils.implForWrapper(blob)._buffer) || undefined; | 
						|
}; | 
						|
 | 
						|
exports.evalVMScript = (window, script) => { | 
						|
  return script.runInContext(idlUtils.implForWrapper(window._document)._global); | 
						|
}; | 
						|
 | 
						|
function processHTML(config) { | 
						|
  const window = exports.jsdom(config.html, config).defaultView; | 
						|
  const implImpl = idlUtils.implForWrapper(window.document.implementation); | 
						|
  const features = JSON.parse(JSON.stringify(implImpl._features)); | 
						|
 | 
						|
  let docsLoaded = 0; | 
						|
  const totalDocs = config.scripts.length + config.src.length; | 
						|
 | 
						|
  if (!window || !window.document) { | 
						|
    reportInitError(new Error("JSDOM: a window object could not be created."), config); | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  function scriptComplete() { | 
						|
    docsLoaded++; | 
						|
 | 
						|
    if (docsLoaded >= totalDocs) { | 
						|
      implImpl._features = features; | 
						|
 | 
						|
      process.nextTick(() => { | 
						|
        if (config.onload) { | 
						|
          config.onload(window); | 
						|
        } | 
						|
        if (config.done) { | 
						|
          config.done(null, window); | 
						|
        } | 
						|
      }); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  function handleScriptError() { | 
						|
    // nextTick so that an exception within scriptComplete won't cause | 
						|
    // another script onerror (which would be an infinite loop) | 
						|
    process.nextTick(scriptComplete); | 
						|
  } | 
						|
 | 
						|
  if (config.scripts.length > 0 || config.src.length > 0) { | 
						|
    implImpl._addFeature("FetchExternalResources", ["script"]); | 
						|
 | 
						|
    for (const scriptSrc of config.scripts) { | 
						|
      const script = window.document.createElement("script"); | 
						|
      script.className = "jsdom"; | 
						|
      script.onload = scriptComplete; | 
						|
      script.onerror = handleScriptError; | 
						|
      script.src = scriptSrc; | 
						|
 | 
						|
      window.document.body.appendChild(script); | 
						|
    } | 
						|
 | 
						|
    for (const scriptText of config.src) { | 
						|
      const script = window.document.createElement("script"); | 
						|
      script.onload = scriptComplete; | 
						|
      script.onerror = handleScriptError; | 
						|
      script.text = scriptText; | 
						|
 | 
						|
      window.document.documentElement.appendChild(script); | 
						|
      window.document.documentElement.removeChild(script); | 
						|
    } | 
						|
  } else if (window.document.readyState === "complete") { | 
						|
    scriptComplete(); | 
						|
  } else { | 
						|
    window.addEventListener("load", scriptComplete); | 
						|
  } | 
						|
} | 
						|
 | 
						|
function setGlobalDefaultConfig(config) { | 
						|
  config.parseOptions = { locationInfo: true }; | 
						|
 | 
						|
  config.pool = config.pool !== undefined ? config.pool : { maxSockets: 6 }; | 
						|
 | 
						|
  config.agentOptions = config.agentOptions !== undefined ? | 
						|
                        config.agentOptions : | 
						|
                        { keepAlive: true, keepAliveMsecs: 115 * 1000 }; | 
						|
 | 
						|
  config.strictSSL = config.strictSSL !== undefined ? config.strictSSL : true; | 
						|
 | 
						|
  config.userAgent = config.userAgent || | 
						|
    `Node.js (${process.platform}; U; rv:${process.version}) AppleWebKit/537.36 (KHTML, like Gecko)`; | 
						|
} | 
						|
 | 
						|
function getConfigFromEnvArguments(args) { | 
						|
  const config = {}; | 
						|
  if (typeof args[0] === "object") { | 
						|
    Object.assign(config, args[0]); | 
						|
  } else { | 
						|
    for (const arg of args) { | 
						|
      switch (typeof arg) { | 
						|
        case "string": | 
						|
          config.somethingToAutodetect = arg; | 
						|
          break; | 
						|
        case "function": | 
						|
          config.done = arg; | 
						|
          break; | 
						|
        case "object": | 
						|
          if (Array.isArray(arg)) { | 
						|
            config.scripts = arg; | 
						|
          } else { | 
						|
            Object.assign(config, arg); | 
						|
          } | 
						|
          break; | 
						|
      } | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  if (!config.done && !config.created && !config.onload) { | 
						|
    throw new Error("Must pass a \"created\", \"onload\", or \"done\" option, or a callback, to jsdom.env"); | 
						|
  } | 
						|
 | 
						|
  if (config.somethingToAutodetect === undefined && | 
						|
      config.html === undefined && !config.file && !config.url) { | 
						|
    throw new Error("Must pass a \"html\", \"file\", or \"url\" option, or a string, to jsdom.env"); | 
						|
  } | 
						|
 | 
						|
  config.scripts = ensureArray(config.scripts); | 
						|
  config.src = ensureArray(config.src); | 
						|
  config.parsingMode = config.parsingMode || "auto"; | 
						|
 | 
						|
  config.features = config.features || { | 
						|
    FetchExternalResources: false, | 
						|
    SkipExternalResources: false, | 
						|
    ProcessExternalResources: false // needed since we'll process it inside jsdom.jsdom() | 
						|
  }; | 
						|
 | 
						|
  if (!config.url && config.file) { | 
						|
    config.url = toFileUrl(config.file); | 
						|
  } | 
						|
 | 
						|
  config.defaultEncoding = config.defaultEncoding || "windows-1252"; | 
						|
 | 
						|
  setGlobalDefaultConfig(config); | 
						|
 | 
						|
  if (config.scripts.length > 0 || config.src.length > 0) { | 
						|
    config.features.ProcessExternalResources = ["script"]; | 
						|
  } | 
						|
  return config; | 
						|
} | 
						|
 | 
						|
function reportInitError(err, config) { | 
						|
  if (config.created) { | 
						|
    config.created(err); | 
						|
  } | 
						|
  if (config.done) { | 
						|
    config.done(err); | 
						|
  } | 
						|
} | 
						|
 | 
						|
function ensureArray(value) { | 
						|
  let array = value || []; | 
						|
  if (typeof array === "string") { | 
						|
    array = [array]; | 
						|
  } | 
						|
  return array; | 
						|
} | 
						|
 | 
						|
function setParsingModeFromExtension(config, filename) { | 
						|
  if (config.parsingMode === "auto") { | 
						|
    const ext = path.extname(filename); | 
						|
    if (ext === ".xhtml" || ext === ".xml") { | 
						|
      config.parsingMode = "xml"; | 
						|
    } | 
						|
  } | 
						|
}
 | 
						|
 |