api.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. "use strict";
  2. const path = require("path");
  3. const fs = require("fs").promises;
  4. const vm = require("vm");
  5. const toughCookie = require("tough-cookie");
  6. const sniffHTMLEncoding = require("html-encoding-sniffer");
  7. const whatwgURL = require("whatwg-url");
  8. const whatwgEncoding = require("whatwg-encoding");
  9. const { URL } = require("whatwg-url");
  10. const MIMEType = require("whatwg-mimetype");
  11. const idlUtils = require("./jsdom/living/generated/utils.js");
  12. const VirtualConsole = require("./jsdom/virtual-console.js");
  13. const { createWindow } = require("./jsdom/browser/Window.js");
  14. const { parseIntoDocument } = require("./jsdom/browser/parser");
  15. const { fragmentSerialization } = require("./jsdom/living/domparsing/serialization.js");
  16. const ResourceLoader = require("./jsdom/browser/resources/resource-loader.js");
  17. const NoOpResourceLoader = require("./jsdom/browser/resources/no-op-resource-loader.js");
  18. class CookieJar extends toughCookie.CookieJar {
  19. constructor(store, options) {
  20. // jsdom cookie jars must be loose by default
  21. super(store, { looseMode: true, ...options });
  22. }
  23. }
  24. const window = Symbol("window");
  25. let sharedFragmentDocument = null;
  26. class JSDOM {
  27. constructor(input = "", options = {}) {
  28. const mimeType = new MIMEType(options.contentType === undefined ? "text/html" : options.contentType);
  29. const { html, encoding } = normalizeHTML(input, mimeType);
  30. options = transformOptions(options, encoding, mimeType);
  31. this[window] = createWindow(options.windowOptions);
  32. const documentImpl = idlUtils.implForWrapper(this[window]._document);
  33. options.beforeParse(this[window]._globalProxy);
  34. parseIntoDocument(html, documentImpl);
  35. documentImpl.close();
  36. }
  37. get window() {
  38. // It's important to grab the global proxy, instead of just the result of `createWindow(...)`, since otherwise
  39. // things like `window.eval` don't exist.
  40. return this[window]._globalProxy;
  41. }
  42. get virtualConsole() {
  43. return this[window]._virtualConsole;
  44. }
  45. get cookieJar() {
  46. // TODO NEWAPI move _cookieJar to window probably
  47. return idlUtils.implForWrapper(this[window]._document)._cookieJar;
  48. }
  49. serialize() {
  50. return fragmentSerialization(idlUtils.implForWrapper(this[window]._document), { requireWellFormed: false });
  51. }
  52. nodeLocation(node) {
  53. if (!idlUtils.implForWrapper(this[window]._document)._parseOptions.sourceCodeLocationInfo) {
  54. throw new Error("Location information was not saved for this jsdom. Use includeNodeLocations during creation.");
  55. }
  56. return idlUtils.implForWrapper(node).sourceCodeLocation;
  57. }
  58. getInternalVMContext() {
  59. if (!vm.isContext(this[window])) {
  60. throw new TypeError("This jsdom was not configured to allow script running. " +
  61. "Use the runScripts option during creation.");
  62. }
  63. return this[window];
  64. }
  65. reconfigure(settings) {
  66. if ("windowTop" in settings) {
  67. this[window]._top = settings.windowTop;
  68. }
  69. if ("url" in settings) {
  70. const document = idlUtils.implForWrapper(this[window]._document);
  71. const url = whatwgURL.parseURL(settings.url);
  72. if (url === null) {
  73. throw new TypeError(`Could not parse "${settings.url}" as a URL`);
  74. }
  75. document._URL = url;
  76. document._origin = whatwgURL.serializeURLOrigin(document._URL);
  77. this[window]._sessionHistory.currentEntry.url = url;
  78. }
  79. }
  80. static fragment(string = "") {
  81. if (!sharedFragmentDocument) {
  82. sharedFragmentDocument = (new JSDOM()).window.document;
  83. }
  84. const template = sharedFragmentDocument.createElement("template");
  85. template.innerHTML = string;
  86. return template.content;
  87. }
  88. static fromURL(url, options = {}) {
  89. return Promise.resolve().then(() => {
  90. // Remove the hash while sending this through the research loader fetch().
  91. // It gets added back a few lines down when constructing the JSDOM object.
  92. const parsedURL = new URL(url);
  93. const originalHash = parsedURL.hash;
  94. parsedURL.hash = "";
  95. url = parsedURL.href;
  96. options = normalizeFromURLOptions(options);
  97. const resourceLoader = resourcesToResourceLoader(options.resources);
  98. const resourceLoaderForInitialRequest = resourceLoader.constructor === NoOpResourceLoader ?
  99. new ResourceLoader() :
  100. resourceLoader;
  101. const req = resourceLoaderForInitialRequest.fetch(url, {
  102. accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  103. cookieJar: options.cookieJar,
  104. referrer: options.referrer
  105. });
  106. return req.then(body => {
  107. const res = req.response;
  108. options = Object.assign(options, {
  109. url: req.href + originalHash,
  110. contentType: res.headers["content-type"],
  111. referrer: req.getHeader("referer") ?? undefined
  112. });
  113. return new JSDOM(body, options);
  114. });
  115. });
  116. }
  117. static async fromFile(filename, options = {}) {
  118. options = normalizeFromFileOptions(filename, options);
  119. const buffer = await fs.readFile(filename);
  120. return new JSDOM(buffer, options);
  121. }
  122. }
  123. function normalizeFromURLOptions(options) {
  124. // Checks on options that are invalid for `fromURL`
  125. if (options.url !== undefined) {
  126. throw new TypeError("Cannot supply a url option when using fromURL");
  127. }
  128. if (options.contentType !== undefined) {
  129. throw new TypeError("Cannot supply a contentType option when using fromURL");
  130. }
  131. // Normalization of options which must be done before the rest of the fromURL code can use them, because they are
  132. // given to request()
  133. const normalized = { ...options };
  134. if (options.referrer !== undefined) {
  135. normalized.referrer = (new URL(options.referrer)).href;
  136. }
  137. if (options.cookieJar === undefined) {
  138. normalized.cookieJar = new CookieJar();
  139. }
  140. return normalized;
  141. // All other options don't need to be processed yet, and can be taken care of in the normal course of things when
  142. // `fromURL` calls `new JSDOM(html, options)`.
  143. }
  144. function normalizeFromFileOptions(filename, options) {
  145. const normalized = { ...options };
  146. if (normalized.contentType === undefined) {
  147. const extname = path.extname(filename);
  148. if (extname === ".xhtml" || extname === ".xht" || extname === ".xml") {
  149. normalized.contentType = "application/xhtml+xml";
  150. }
  151. }
  152. if (normalized.url === undefined) {
  153. normalized.url = new URL("file:" + path.resolve(filename));
  154. }
  155. return normalized;
  156. }
  157. function transformOptions(options, encoding, mimeType) {
  158. const transformed = {
  159. windowOptions: {
  160. // Defaults
  161. url: "about:blank",
  162. referrer: "",
  163. contentType: "text/html",
  164. parsingMode: "html",
  165. parseOptions: {
  166. sourceCodeLocationInfo: false,
  167. scriptingEnabled: false
  168. },
  169. runScripts: undefined,
  170. encoding,
  171. pretendToBeVisual: false,
  172. storageQuota: 5000000,
  173. // Defaults filled in later
  174. resourceLoader: undefined,
  175. virtualConsole: undefined,
  176. cookieJar: undefined
  177. },
  178. // Defaults
  179. beforeParse() { }
  180. };
  181. // options.contentType was parsed into mimeType by the caller.
  182. if (!mimeType.isHTML() && !mimeType.isXML()) {
  183. throw new RangeError(`The given content type of "${options.contentType}" was not a HTML or XML content type`);
  184. }
  185. transformed.windowOptions.contentType = mimeType.essence;
  186. transformed.windowOptions.parsingMode = mimeType.isHTML() ? "html" : "xml";
  187. if (options.url !== undefined) {
  188. transformed.windowOptions.url = (new URL(options.url)).href;
  189. }
  190. if (options.referrer !== undefined) {
  191. transformed.windowOptions.referrer = (new URL(options.referrer)).href;
  192. }
  193. if (options.includeNodeLocations) {
  194. if (transformed.windowOptions.parsingMode === "xml") {
  195. throw new TypeError("Cannot set includeNodeLocations to true with an XML content type");
  196. }
  197. transformed.windowOptions.parseOptions = { sourceCodeLocationInfo: true };
  198. }
  199. transformed.windowOptions.cookieJar = options.cookieJar === undefined ?
  200. new CookieJar() :
  201. options.cookieJar;
  202. transformed.windowOptions.virtualConsole = options.virtualConsole === undefined ?
  203. (new VirtualConsole()).sendTo(console) :
  204. options.virtualConsole;
  205. if (!(transformed.windowOptions.virtualConsole instanceof VirtualConsole)) {
  206. throw new TypeError("virtualConsole must be an instance of VirtualConsole");
  207. }
  208. transformed.windowOptions.resourceLoader = resourcesToResourceLoader(options.resources);
  209. if (options.runScripts !== undefined) {
  210. transformed.windowOptions.runScripts = String(options.runScripts);
  211. if (transformed.windowOptions.runScripts === "dangerously") {
  212. transformed.windowOptions.parseOptions.scriptingEnabled = true;
  213. } else if (transformed.windowOptions.runScripts !== "outside-only") {
  214. throw new RangeError(`runScripts must be undefined, "dangerously", or "outside-only"`);
  215. }
  216. }
  217. if (options.beforeParse !== undefined) {
  218. transformed.beforeParse = options.beforeParse;
  219. }
  220. if (options.pretendToBeVisual !== undefined) {
  221. transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual);
  222. }
  223. if (options.storageQuota !== undefined) {
  224. transformed.windowOptions.storageQuota = Number(options.storageQuota);
  225. }
  226. return transformed;
  227. }
  228. function normalizeHTML(html, mimeType) {
  229. let encoding = "UTF-8";
  230. if (ArrayBuffer.isView(html)) {
  231. html = Buffer.from(html.buffer, html.byteOffset, html.byteLength);
  232. } else if (html instanceof ArrayBuffer) {
  233. html = Buffer.from(html);
  234. }
  235. if (Buffer.isBuffer(html)) {
  236. encoding = sniffHTMLEncoding(html, {
  237. defaultEncoding: mimeType.isXML() ? "UTF-8" : "windows-1252",
  238. transportLayerEncodingLabel: mimeType.parameters.get("charset")
  239. });
  240. html = whatwgEncoding.decode(html, encoding);
  241. } else {
  242. html = String(html);
  243. }
  244. return { html, encoding };
  245. }
  246. function resourcesToResourceLoader(resources) {
  247. switch (resources) {
  248. case undefined: {
  249. return new NoOpResourceLoader();
  250. }
  251. case "usable": {
  252. return new ResourceLoader();
  253. }
  254. default: {
  255. if (!(resources instanceof ResourceLoader)) {
  256. throw new TypeError("resources must be an instance of ResourceLoader");
  257. }
  258. return resources;
  259. }
  260. }
  261. }
  262. exports.JSDOM = JSDOM;
  263. exports.VirtualConsole = VirtualConsole;
  264. exports.CookieJar = CookieJar;
  265. exports.ResourceLoader = ResourceLoader;
  266. exports.toughCookie = toughCookie;