api.js 11 KB

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