old-api.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. "use strict";
  2. /* eslint-disable no-unused-expressions */
  3. () => `jsdom 7.x onward only works on Node.js 4 or newer: https://github.com/tmpvar/jsdom#install`;
  4. /* eslint-enable no-unused-expressions */
  5. const fs = require("fs");
  6. const path = require("path");
  7. const { CookieJar } = require("tough-cookie");
  8. const MIMEType = require("whatwg-mimetype");
  9. const { toFileUrl } = require("./jsdom/utils");
  10. const documentFeatures = require("./jsdom/browser/documentfeatures");
  11. const { domToHtml } = require("./jsdom/browser/domtohtml");
  12. const Window = require("./jsdom/browser/Window");
  13. const resourceLoader = require("./jsdom/browser/resource-loader");
  14. const VirtualConsole = require("./jsdom/virtual-console");
  15. const idlUtils = require("./jsdom/living/generated/utils");
  16. const Blob = require("./jsdom/living/generated/Blob");
  17. const whatwgURL = require("whatwg-url");
  18. require("./jsdom/living"); // Enable living standard features
  19. /* eslint-disable no-restricted-modules */
  20. // TODO: stop using the built-in URL in favor of the spec-compliant whatwg-url package
  21. // This legacy usage is in the process of being purged.
  22. const URL = require("url");
  23. /* eslint-enable no-restricted-modules */
  24. const canReadFilesFromFS = Boolean(fs.readFile); // in a browserify environment, this isn't present
  25. exports.createVirtualConsole = function (options) {
  26. return new VirtualConsole(options);
  27. };
  28. exports.getVirtualConsole = function (window) {
  29. return window._virtualConsole;
  30. };
  31. exports.createCookieJar = function () {
  32. return new CookieJar(null, { looseMode: true });
  33. };
  34. exports.nodeLocation = function (node) {
  35. return idlUtils.implForWrapper(node).__location;
  36. };
  37. exports.reconfigureWindow = function (window, newProps) {
  38. if ("top" in newProps) {
  39. window._top = newProps.top;
  40. }
  41. };
  42. exports.changeURL = function (window, urlString) {
  43. const doc = idlUtils.implForWrapper(window._document);
  44. const url = whatwgURL.parseURL(urlString);
  45. if (url === null) {
  46. throw new TypeError(`Could not parse "${urlString}" as a URL`);
  47. }
  48. doc._URL = url;
  49. doc.origin = whatwgURL.serializeURLOrigin(doc._URL);
  50. };
  51. // Proxy to features module
  52. Object.defineProperty(exports, "defaultDocumentFeatures", {
  53. enumerable: true,
  54. configurable: true,
  55. get() {
  56. return documentFeatures.defaultDocumentFeatures;
  57. },
  58. set(v) {
  59. documentFeatures.defaultDocumentFeatures = v;
  60. }
  61. });
  62. exports.jsdom = function (html, options) {
  63. if (options === undefined) {
  64. options = {};
  65. }
  66. if (options.parsingMode === undefined || options.parsingMode === "auto") {
  67. options.parsingMode = "html";
  68. }
  69. if (options.parsingMode !== "html" && options.parsingMode !== "xml") {
  70. throw new RangeError(`Invalid parsingMode option ${JSON.stringify(options.parsingMode)}; must be either "html", ` +
  71. `"xml", "auto", or undefined`);
  72. }
  73. options.encoding = options.encoding || "UTF-8";
  74. setGlobalDefaultConfig(options);
  75. // Back-compat hack: we have previously suggested nesting these under document, for jsdom.env at least.
  76. // So we need to support that.
  77. if (options.document) {
  78. if (options.document.cookie !== undefined) {
  79. options.cookie = options.document.cookie;
  80. }
  81. if (options.document.referrer !== undefined) {
  82. options.referrer = options.document.referrer;
  83. }
  84. }
  85. // Adapt old API `features: { ProcessExternalResources: ["script"] }` to the runScripts option.
  86. // This is part of a larger effort to eventually remove the document features infrastructure entirely. It's unclear
  87. // whether we'll kill the old API or document features first, but as long as old API survives, attempts to kill
  88. // document features will need this kind of adapter.
  89. if (!options.features) {
  90. options.features = exports.defaultDocumentFeatures;
  91. }
  92. if (options.features.ProcessExternalResources === undefined) {
  93. options.features.ProcessExternalResources = ["script"];
  94. }
  95. const ProcessExternalResources = options.features.ProcessExternalResources || [];
  96. if (ProcessExternalResources === "script" ||
  97. (ProcessExternalResources.includes && ProcessExternalResources.includes("script"))) {
  98. options.runScripts = "dangerously";
  99. }
  100. if (options.pretendToBeVisual !== undefined) {
  101. options.pretendToBeVisual = Boolean(options.pretendToBeVisual);
  102. } else {
  103. options.pretendToBeVisual = false;
  104. }
  105. options.storageQuota = options.storageQuota || 5000000;
  106. // List options explicitly to be clear which are passed through
  107. const window = new Window({
  108. parsingMode: options.parsingMode,
  109. parseOptions: options.parseOptions,
  110. contentType: options.contentType,
  111. encoding: options.encoding,
  112. url: options.url,
  113. lastModified: options.lastModified,
  114. referrer: options.referrer,
  115. cookieJar: options.cookieJar,
  116. cookie: options.cookie,
  117. resourceLoader: options.resourceLoader,
  118. deferClose: options.deferClose,
  119. concurrentNodeIterators: options.concurrentNodeIterators,
  120. virtualConsole: options.virtualConsole,
  121. pool: options.pool,
  122. agent: options.agent,
  123. agentClass: options.agentClass,
  124. agentOptions: options.agentOptions,
  125. strictSSL: options.strictSSL,
  126. proxy: options.proxy,
  127. userAgent: options.userAgent,
  128. runScripts: options.runScripts,
  129. pretendToBeVisual: options.pretendToBeVisual,
  130. storageQuota: options.storageQuota
  131. });
  132. const documentImpl = idlUtils.implForWrapper(window.document);
  133. documentFeatures.applyDocumentFeatures(documentImpl, options.features);
  134. if (options.created) {
  135. options.created(null, window.document.defaultView);
  136. }
  137. if (options.parsingMode === "html") {
  138. if (html === undefined || html === "") {
  139. html = "<html><head></head><body></body></html>";
  140. }
  141. window.document.write(html);
  142. } else if (options.parsingMode === "xml") {
  143. if (html !== undefined) {
  144. documentImpl._htmlToDom.appendToDocument(html, documentImpl);
  145. }
  146. }
  147. if (window.document.close && !options.deferClose) {
  148. window.document.close();
  149. }
  150. return window.document;
  151. };
  152. exports.jQueryify = exports.jsdom.jQueryify = function (window, jqueryUrl, callback) {
  153. if (!window || !window.document) {
  154. return;
  155. }
  156. const implImpl = idlUtils.implForWrapper(window.document.implementation);
  157. const oldFeatures = implImpl._features;
  158. const oldRunScripts = window._runScripts;
  159. implImpl._addFeature("FetchExternalResources", ["script"]);
  160. documentFeatures.contextifyWindow(idlUtils.implForWrapper(window.document)._global);
  161. window._runScripts = "dangerously";
  162. const scriptEl = window.document.createElement("script");
  163. scriptEl.className = "jsdom";
  164. scriptEl.src = jqueryUrl;
  165. scriptEl.onload = scriptEl.onerror = () => {
  166. implImpl._features = oldFeatures;
  167. window._runScripts = oldRunScripts;
  168. // Can't un-contextify the window. Oh well. That's what we get for such magic behavior in old API.
  169. if (callback) {
  170. callback(window, window.jQuery);
  171. }
  172. };
  173. window.document.body.appendChild(scriptEl);
  174. };
  175. exports.env = exports.jsdom.env = function () {
  176. const config = getConfigFromEnvArguments(arguments);
  177. let req = null;
  178. if (config.file && canReadFilesFromFS) {
  179. req = resourceLoader.readFile(
  180. config.file,
  181. { defaultEncoding: config.defaultEncoding, detectMetaCharset: true },
  182. (err, text, res) => {
  183. if (err) {
  184. reportInitError(err, config);
  185. return;
  186. }
  187. const contentType = new MIMEType(res.headers["content-type"]);
  188. config.encoding = contentType.parameters.get("charset");
  189. setParsingModeFromExtension(config, config.file);
  190. config.html = text;
  191. processHTML(config);
  192. }
  193. );
  194. } else if (config.html !== undefined) {
  195. processHTML(config);
  196. } else if (config.url) {
  197. req = handleUrl(config);
  198. } else if (config.somethingToAutodetect !== undefined) {
  199. const url = URL.parse(config.somethingToAutodetect);
  200. if (url.protocol && url.hostname) {
  201. config.url = config.somethingToAutodetect;
  202. req = handleUrl(config.somethingToAutodetect);
  203. } else if (canReadFilesFromFS) {
  204. try {
  205. req = resourceLoader.readFile(
  206. config.somethingToAutodetect,
  207. { defaultEncoding: config.defaultEncoding, detectMetaCharset: true },
  208. (err, text, res) => {
  209. if (err) {
  210. if (err.code === "ENOENT" || err.code === "ENAMETOOLONG" || err.code === "ERR_INVALID_ARG_TYPE") {
  211. config.html = config.somethingToAutodetect;
  212. processHTML(config);
  213. } else {
  214. reportInitError(err, config);
  215. }
  216. } else {
  217. const contentType = new MIMEType(res.headers["content-type"]);
  218. config.encoding = contentType.parameters.get("charset");
  219. setParsingModeFromExtension(config, config.somethingToAutodetect);
  220. config.html = text;
  221. config.url = toFileUrl(config.somethingToAutodetect);
  222. processHTML(config);
  223. }
  224. }
  225. );
  226. } catch (err) {
  227. config.html = config.somethingToAutodetect;
  228. processHTML(config);
  229. }
  230. } else {
  231. config.html = config.somethingToAutodetect;
  232. processHTML(config);
  233. }
  234. }
  235. function handleUrl() {
  236. config.cookieJar = config.cookieJar || exports.createCookieJar();
  237. const options = {
  238. defaultEncoding: config.defaultEncoding,
  239. detectMetaCharset: true,
  240. headers: config.headers,
  241. pool: config.pool,
  242. strictSSL: config.strictSSL,
  243. proxy: config.proxy,
  244. cookieJar: config.cookieJar,
  245. userAgent: config.userAgent,
  246. agent: config.agent,
  247. agentClass: config.agentClass,
  248. agentOptions: config.agentOptions
  249. };
  250. const { fragment } = whatwgURL.parseURL(config.url);
  251. return resourceLoader.download(config.url, options, (err, responseText, res) => {
  252. if (err) {
  253. reportInitError(err, config);
  254. return;
  255. }
  256. // The use of `res.request.uri.href` ensures that `window.location.href`
  257. // is updated when `request` follows redirects.
  258. config.html = responseText;
  259. config.url = res.request.uri.href;
  260. if (fragment) {
  261. config.url += `#${fragment}`;
  262. }
  263. if (res.headers["last-modified"]) {
  264. config.lastModified = new Date(res.headers["last-modified"]);
  265. }
  266. const contentType = new MIMEType(res.headers["content-type"]);
  267. if (config.parsingMode === "auto") {
  268. if (contentType.isXML()) {
  269. config.parsingMode = "xml";
  270. }
  271. }
  272. config.contentType = contentType.essence;
  273. config.encoding = contentType.parameters.get("charset");
  274. processHTML(config);
  275. });
  276. }
  277. return req;
  278. };
  279. exports.serializeDocument = function (doc) {
  280. return domToHtml([idlUtils.implForWrapper(doc)]);
  281. };
  282. exports.blobToBuffer = function (blob) {
  283. return (Blob.is(blob) && idlUtils.implForWrapper(blob)._buffer) || undefined;
  284. };
  285. exports.evalVMScript = (window, script) => {
  286. return script.runInContext(idlUtils.implForWrapper(window._document)._global);
  287. };
  288. function processHTML(config) {
  289. const window = exports.jsdom(config.html, config).defaultView;
  290. const implImpl = idlUtils.implForWrapper(window.document.implementation);
  291. const features = JSON.parse(JSON.stringify(implImpl._features));
  292. let docsLoaded = 0;
  293. const totalDocs = config.scripts.length + config.src.length;
  294. if (!window || !window.document) {
  295. reportInitError(new Error("JSDOM: a window object could not be created."), config);
  296. return;
  297. }
  298. function scriptComplete() {
  299. docsLoaded++;
  300. if (docsLoaded >= totalDocs) {
  301. implImpl._features = features;
  302. process.nextTick(() => {
  303. if (config.onload) {
  304. config.onload(window);
  305. }
  306. if (config.done) {
  307. config.done(null, window);
  308. }
  309. });
  310. }
  311. }
  312. function handleScriptError() {
  313. // nextTick so that an exception within scriptComplete won't cause
  314. // another script onerror (which would be an infinite loop)
  315. process.nextTick(scriptComplete);
  316. }
  317. if (config.scripts.length > 0 || config.src.length > 0) {
  318. implImpl._addFeature("FetchExternalResources", ["script"]);
  319. for (const scriptSrc of config.scripts) {
  320. const script = window.document.createElement("script");
  321. script.className = "jsdom";
  322. script.onload = scriptComplete;
  323. script.onerror = handleScriptError;
  324. script.src = scriptSrc;
  325. window.document.body.appendChild(script);
  326. }
  327. for (const scriptText of config.src) {
  328. const script = window.document.createElement("script");
  329. script.onload = scriptComplete;
  330. script.onerror = handleScriptError;
  331. script.text = scriptText;
  332. window.document.documentElement.appendChild(script);
  333. window.document.documentElement.removeChild(script);
  334. }
  335. } else if (window.document.readyState === "complete") {
  336. scriptComplete();
  337. } else {
  338. window.addEventListener("load", scriptComplete);
  339. }
  340. }
  341. function setGlobalDefaultConfig(config) {
  342. config.parseOptions = { locationInfo: true };
  343. config.pool = config.pool !== undefined ? config.pool : { maxSockets: 6 };
  344. config.agentOptions = config.agentOptions !== undefined ?
  345. config.agentOptions :
  346. { keepAlive: true, keepAliveMsecs: 115 * 1000 };
  347. config.strictSSL = config.strictSSL !== undefined ? config.strictSSL : true;
  348. config.userAgent = config.userAgent ||
  349. `Node.js (${process.platform}; U; rv:${process.version}) AppleWebKit/537.36 (KHTML, like Gecko)`;
  350. }
  351. function getConfigFromEnvArguments(args) {
  352. const config = {};
  353. if (typeof args[0] === "object") {
  354. Object.assign(config, args[0]);
  355. } else {
  356. for (const arg of args) {
  357. switch (typeof arg) {
  358. case "string":
  359. config.somethingToAutodetect = arg;
  360. break;
  361. case "function":
  362. config.done = arg;
  363. break;
  364. case "object":
  365. if (Array.isArray(arg)) {
  366. config.scripts = arg;
  367. } else {
  368. Object.assign(config, arg);
  369. }
  370. break;
  371. }
  372. }
  373. }
  374. if (!config.done && !config.created && !config.onload) {
  375. throw new Error("Must pass a \"created\", \"onload\", or \"done\" option, or a callback, to jsdom.env");
  376. }
  377. if (config.somethingToAutodetect === undefined &&
  378. config.html === undefined && !config.file && !config.url) {
  379. throw new Error("Must pass a \"html\", \"file\", or \"url\" option, or a string, to jsdom.env");
  380. }
  381. config.scripts = ensureArray(config.scripts);
  382. config.src = ensureArray(config.src);
  383. config.parsingMode = config.parsingMode || "auto";
  384. config.features = config.features || {
  385. FetchExternalResources: false,
  386. SkipExternalResources: false,
  387. ProcessExternalResources: false // needed since we'll process it inside jsdom.jsdom()
  388. };
  389. if (!config.url && config.file) {
  390. config.url = toFileUrl(config.file);
  391. }
  392. config.defaultEncoding = config.defaultEncoding || "windows-1252";
  393. setGlobalDefaultConfig(config);
  394. if (config.scripts.length > 0 || config.src.length > 0) {
  395. config.features.ProcessExternalResources = ["script"];
  396. }
  397. return config;
  398. }
  399. function reportInitError(err, config) {
  400. if (config.created) {
  401. config.created(err);
  402. }
  403. if (config.done) {
  404. config.done(err);
  405. }
  406. }
  407. function ensureArray(value) {
  408. let array = value || [];
  409. if (typeof array === "string") {
  410. array = [array];
  411. }
  412. return array;
  413. }
  414. function setParsingModeFromExtension(config, filename) {
  415. if (config.parsingMode === "auto") {
  416. const ext = path.extname(filename);
  417. if (ext === ".xhtml" || ext === ".xml") {
  418. config.parsingMode = "xml";
  419. }
  420. }
  421. }