UNPKG

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