UNPKG

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