UNPKG

10.5 kBJavaScriptView Raw
1"use strict";
2const path = require("path");
3const fs = require("pn/fs");
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 Window = 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, Object.assign({ 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] = new Window(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 `new Window(...)`, since otherwise things
49 // 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 fromFile(filename, options = {}) {
148 return Promise.resolve().then(() => {
149 options = normalizeFromFileOptions(filename, options);
150
151 return fs.readFile(filename).then(buffer => {
152 return new JSDOM(buffer, options);
153 });
154 });
155 }
156}
157
158function normalizeFromURLOptions(options) {
159 // Checks on options that are invalid for `fromURL`
160 if (options.url !== undefined) {
161 throw new TypeError("Cannot supply a url option when using fromURL");
162 }
163 if (options.contentType !== undefined) {
164 throw new TypeError("Cannot supply a contentType option when using fromURL");
165 }
166
167 // Normalization of options which must be done before the rest of the fromURL code can use them, because they are
168 // given to request()
169 const normalized = Object.assign({}, options);
170
171 if (options.referrer !== undefined) {
172 normalized.referrer = (new URL(options.referrer)).href;
173 }
174
175 if (options.cookieJar === undefined) {
176 normalized.cookieJar = new CookieJar();
177 }
178
179 return normalized;
180
181 // All other options don't need to be processed yet, and can be taken care of in the normal course of things when
182 // `fromURL` calls `new JSDOM(html, options)`.
183}
184
185function normalizeFromFileOptions(filename, options) {
186 const normalized = Object.assign({}, options);
187
188 if (normalized.contentType === undefined) {
189 const extname = path.extname(filename);
190 if (extname === ".xhtml" || extname === ".xht" || extname === ".xml") {
191 normalized.contentType = "application/xhtml+xml";
192 }
193 }
194
195 if (normalized.url === undefined) {
196 normalized.url = new URL("file:" + path.resolve(filename));
197 }
198
199 return normalized;
200}
201
202function transformOptions(options, encoding, mimeType) {
203 const transformed = {
204 windowOptions: {
205 // Defaults
206 url: "about:blank",
207 referrer: "",
208 contentType: "text/html",
209 parsingMode: "html",
210 parseOptions: { sourceCodeLocationInfo: false },
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.runScripts !== "outside-only") {
268 throw new RangeError(`runScripts must be undefined, "dangerously", or "outside-only"`);
269 }
270 }
271
272 if (options.beforeParse !== undefined) {
273 transformed.beforeParse = options.beforeParse;
274 }
275
276 if (options.pretendToBeVisual !== undefined) {
277 transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual);
278 }
279
280 if (options.storageQuota !== undefined) {
281 transformed.windowOptions.storageQuota = Number(options.storageQuota);
282 }
283
284 // concurrentNodeIterators??
285
286 return transformed;
287}
288
289function normalizeHTML(html = "", mimeType) {
290 let encoding = "UTF-8";
291
292 if (ArrayBuffer.isView(html)) {
293 html = Buffer.from(html.buffer, html.byteOffset, html.byteLength);
294 } else if (html instanceof ArrayBuffer) {
295 html = Buffer.from(html);
296 }
297
298 if (Buffer.isBuffer(html)) {
299 encoding = sniffHTMLEncoding(html, {
300 defaultEncoding: mimeType.isXML() ? "UTF-8" : "windows-1252",
301 transportLayerEncodingLabel: mimeType.parameters.get("charset")
302 });
303 html = whatwgEncoding.decode(html, encoding);
304 } else {
305 html = String(html);
306 }
307
308 return { html, encoding };
309}
310
311function resourcesToResourceLoader(resources) {
312 switch (resources) {
313 case undefined: {
314 return new NoOpResourceLoader();
315 }
316 case "usable": {
317 return new ResourceLoader();
318 }
319 default: {
320 if (!(resources instanceof ResourceLoader)) {
321 throw new TypeError("resources must be an instance of ResourceLoader");
322 }
323 return resources;
324 }
325 }
326}
327
328exports.JSDOM = JSDOM;
329
330exports.VirtualConsole = VirtualConsole;
331exports.CookieJar = CookieJar;
332exports.ResourceLoader = ResourceLoader;
333
334exports.toughCookie = toughCookie;