api.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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. }
  78. }
  79. static fragment(string = "") {
  80. if (!sharedFragmentDocument) {
  81. sharedFragmentDocument = (new JSDOM()).window.document;
  82. }
  83. const template = sharedFragmentDocument.createElement("template");
  84. template.innerHTML = string;
  85. return template.content;
  86. }
  87. static fromURL(url, options = {}) {
  88. return Promise.resolve().then(() => {
  89. // Remove the hash while sending this through the research loader fetch().
  90. // It gets added back a few lines down when constructing the JSDOM object.
  91. const parsedURL = new URL(url);
  92. const originalHash = parsedURL.hash;
  93. parsedURL.hash = "";
  94. url = parsedURL.href;
  95. options = normalizeFromURLOptions(options);
  96. const resourceLoader = resourcesToResourceLoader(options.resources);
  97. const resourceLoaderForInitialRequest = resourceLoader.constructor === NoOpResourceLoader ?
  98. new ResourceLoader() :
  99. resourceLoader;
  100. const req = resourceLoaderForInitialRequest.fetch(url, {
  101. accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  102. cookieJar: options.cookieJar,
  103. referrer: options.referrer
  104. });
  105. return req.then(body => {
  106. const res = req.response;
  107. options = Object.assign(options, {
  108. url: req.href + originalHash,
  109. contentType: res.headers["content-type"],
  110. referrer: req.getHeader("referer")
  111. });
  112. return new JSDOM(body, options);
  113. });
  114. });
  115. }
  116. static async fromFile(filename, options = {}) {
  117. options = normalizeFromFileOptions(filename, options);
  118. const buffer = await fs.readFile(filename);
  119. return new JSDOM(buffer, options);
  120. }
  121. }
  122. function normalizeFromURLOptions(options) {
  123. // Checks on options that are invalid for `fromURL`
  124. if (options.url !== undefined) {
  125. throw new TypeError("Cannot supply a url option when using fromURL");
  126. }
  127. if (options.contentType !== undefined) {
  128. throw new TypeError("Cannot supply a contentType option when using fromURL");
  129. }
  130. // Normalization of options which must be done before the rest of the fromURL code can use them, because they are
  131. // given to request()
  132. const normalized = { ...options };
  133. if (options.referrer !== undefined) {
  134. normalized.referrer = (new URL(options.referrer)).href;
  135. }
  136. if (options.cookieJar === undefined) {
  137. normalized.cookieJar = new CookieJar();
  138. }
  139. return normalized;
  140. // All other options don't need to be processed yet, and can be taken care of in the normal course of things when
  141. // `fromURL` calls `new JSDOM(html, options)`.
  142. }
  143. function normalizeFromFileOptions(filename, options) {
  144. const normalized = { ...options };
  145. if (normalized.contentType === undefined) {
  146. const extname = path.extname(filename);
  147. if (extname === ".xhtml" || extname === ".xht" || extname === ".xml") {
  148. normalized.contentType = "application/xhtml+xml";
  149. }
  150. }
  151. if (normalized.url === undefined) {
  152. normalized.url = new URL("file:" + path.resolve(filename));
  153. }
  154. return normalized;
  155. }
  156. function transformOptions(options, encoding, mimeType) {
  157. const transformed = {
  158. windowOptions: {
  159. // Defaults
  160. url: "about:blank",
  161. referrer: "",
  162. contentType: "text/html",
  163. parsingMode: "html",
  164. parseOptions: {
  165. sourceCodeLocationInfo: false,
  166. scriptingEnabled: false
  167. },
  168. runScripts: undefined,
  169. encoding,
  170. pretendToBeVisual: false,
  171. storageQuota: 5000000,
  172. // Defaults filled in later
  173. resourceLoader: undefined,
  174. virtualConsole: undefined,
  175. cookieJar: undefined
  176. },
  177. // Defaults
  178. beforeParse() { }
  179. };
  180. // options.contentType was parsed into mimeType by the caller.
  181. if (!mimeType.isHTML() && !mimeType.isXML()) {
  182. throw new RangeError(`The given content type of "${options.contentType}" was not a HTML or XML content type`);
  183. }
  184. transformed.windowOptions.contentType = mimeType.essence;
  185. transformed.windowOptions.parsingMode = mimeType.isHTML() ? "html" : "xml";
  186. if (options.url !== undefined) {
  187. transformed.windowOptions.url = (new URL(options.url)).href;
  188. }
  189. if (options.referrer !== undefined) {
  190. transformed.windowOptions.referrer = (new URL(options.referrer)).href;
  191. }
  192. if (options.includeNodeLocations) {
  193. if (transformed.windowOptions.parsingMode === "xml") {
  194. throw new TypeError("Cannot set includeNodeLocations to true with an XML content type");
  195. }
  196. transformed.windowOptions.parseOptions = { sourceCodeLocationInfo: true };
  197. }
  198. transformed.windowOptions.cookieJar = options.cookieJar === undefined ?
  199. new CookieJar() :
  200. options.cookieJar;
  201. transformed.windowOptions.virtualConsole = options.virtualConsole === undefined ?
  202. (new VirtualConsole()).sendTo(console) :
  203. options.virtualConsole;
  204. if (!(transformed.windowOptions.virtualConsole instanceof VirtualConsole)) {
  205. throw new TypeError("virtualConsole must be an instance of VirtualConsole");
  206. }
  207. transformed.windowOptions.resourceLoader = resourcesToResourceLoader(options.resources);
  208. if (options.runScripts !== undefined) {
  209. transformed.windowOptions.runScripts = String(options.runScripts);
  210. if (transformed.windowOptions.runScripts === "dangerously") {
  211. transformed.windowOptions.parseOptions.scriptingEnabled = true;
  212. } else if (transformed.windowOptions.runScripts !== "outside-only") {
  213. throw new RangeError(`runScripts must be undefined, "dangerously", or "outside-only"`);
  214. }
  215. }
  216. if (options.beforeParse !== undefined) {
  217. transformed.beforeParse = options.beforeParse;
  218. }
  219. if (options.pretendToBeVisual !== undefined) {
  220. transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual);
  221. }
  222. if (options.storageQuota !== undefined) {
  223. transformed.windowOptions.storageQuota = Number(options.storageQuota);
  224. }
  225. return transformed;
  226. }
  227. function normalizeHTML(html, mimeType) {
  228. let encoding = "UTF-8";
  229. if (ArrayBuffer.isView(html)) {
  230. html = Buffer.from(html.buffer, html.byteOffset, html.byteLength);
  231. } else if (html instanceof ArrayBuffer) {
  232. html = Buffer.from(html);
  233. }
  234. if (Buffer.isBuffer(html)) {
  235. encoding = sniffHTMLEncoding(html, {
  236. defaultEncoding: mimeType.isXML() ? "UTF-8" : "windows-1252",
  237. transportLayerEncodingLabel: mimeType.parameters.get("charset")
  238. });
  239. html = whatwgEncoding.decode(html, encoding);
  240. } else {
  241. html = String(html);
  242. }
  243. return { html, encoding };
  244. }
  245. function resourcesToResourceLoader(resources) {
  246. switch (resources) {
  247. case undefined: {
  248. return new NoOpResourceLoader();
  249. }
  250. case "usable": {
  251. return new ResourceLoader();
  252. }
  253. default: {
  254. if (!(resources instanceof ResourceLoader)) {
  255. throw new TypeError("resources must be an instance of ResourceLoader");
  256. }
  257. return resources;
  258. }
  259. }
  260. }
  261. exports.JSDOM = JSDOM;
  262. exports.VirtualConsole = VirtualConsole;
  263. exports.CookieJar = CookieJar;
  264. exports.ResourceLoader = ResourceLoader;
  265. exports.toughCookie = toughCookie;