api.js 11 KB

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