UNPKG

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