Permalink
Newer
100644
707 lines (612 sloc)
21.4 KB
3
const webIDLConversions = require("webidl-conversions");
4
const { CSSStyleDeclaration } = require("cssstyle");
5
const { Performance: RawPerformance } = require("w3c-hr-time");
6
const notImplemented = require("./not-implemented");
8
const Element = require("../living/generated/Element");
9
const EventTarget = require("../living/generated/EventTarget");
10
const PageTransitionEvent = require("../living/generated/PageTransitionEvent");
11
const namedPropertiesWindow = require("../living/named-properties-window");
12
const cssom = require("cssom");
13
const postMessage = require("../living/post-message");
16
const idlUtils = require("../living/generated/utils");
17
const createXMLHttpRequest = require("../living/xmlhttprequest");
18
const createFileReader = require("../living/generated/FileReader").createInterface;
19
const createWebSocket = require("../living/generated/WebSocket").createInterface;
20
const WebSocketImpl = require("../living/websockets/WebSocket-impl").implementation;
21
const BarProp = require("../living/generated/BarProp");
22
const Document = require("../living/generated/Document");
23
const External = require("../living/generated/External");
24
const Navigator = require("../living/generated/Navigator");
25
const Performance = require("../living/generated/Performance");
26
const Screen = require("../living/generated/Screen");
27
const Storage = require("../living/generated/Storage");
28
const createAbortController = require("../living/generated/AbortController").createInterface;
29
const createAbortSignal = require("../living/generated/AbortSignal").createInterface;
30
const reportException = require("../living/helpers/runtime-script-errors");
31
const { matchesDontThrow } = require("../living/helpers/selectors");
32
const { fireAnEvent } = require("../living/helpers/events");
33
const SessionHistory = require("../living/window/SessionHistory");
35
const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
36
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;
37
38
const defaultStyleSheet = require("./default-stylesheet");
39
let parsedDefaultStyleSheet;
40
41
// NB: the require() must be after assigning `module.exports` because this require() is circular
42
// TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up.
44
const dom = require("../living");
48
// NOTE: per https://heycam.github.io/webidl/#Global, all properties on the Window object must be own-properties.
49
// That is why we assign everything inside of the constructor, instead of using a shared prototype.
50
// You can verify this in e.g. Firefox or Internet Explorer, which do a good job with Web IDL compliance.
56
const windowInitialized = rawPerformance.now();
60
mixin(window, WindowEventHandlersImpl.prototype);
61
mixin(window, GlobalEventHandlersImpl.prototype);
62
63
this._initGlobalEvents();
64
65
///// INTERFACES FROM THE DOM
66
// TODO: consider a mode of some sort where these are not shared between all DOM instances
67
// It'd be very memory-expensive in most cases, though.
68
for (const name in dom) {
69
Object.defineProperty(window, name, {
70
enumerable: false,
71
configurable: true,
72
writable: true,
73
value: dom[name]
74
});
75
}
77
///// PRIVATE DATA PROPERTIES
79
this._resourceLoader = options.resourceLoader;
80
81
// vm initialization is deferred until script processing is activated
83
Object.defineProperty(idlUtils.implForWrapper(this), idlUtils.wrapperSymbol, { get: () => this._globalProxy });
85
let timers = Object.create(null);
86
let animationFrameCallbacks = Object.create(null);
88
// List options explicitly to be clear which are passed through
89
this._document = Document.create([], {
90
options: {
91
parsingMode: options.parsingMode,
92
contentType: options.contentType,
94
cookieJar: options.cookieJar,
95
url: options.url,
96
lastModified: options.lastModified,
97
referrer: options.referrer,
98
concurrentNodeIterators: options.concurrentNodeIterators,
100
defaultView: this._globalProxy,
101
global: this
102
}
103
});
104
// https://html.spec.whatwg.org/#session-history
105
this._sessionHistory = new SessionHistory({
106
document: idlUtils.implForWrapper(this._document),
107
url: idlUtils.implForWrapper(this._document)._URL,
108
stateObject: null
113
this._runScripts = options.runScripts;
114
if (this._runScripts === "outside-only" || this._runScripts === "dangerously") {
115
contextifyWindow(this);
116
}
117
118
// Set up the window as if it's a top level window.
119
// If it's not, then references will be corrected by frame/iframe code.
120
this._parent = this._top = this._globalProxy;
121
this._frameElement = null;
122
123
// This implements window.frames.length, since window.frames returns a
124
// self reference to the window object. This value is incremented in the
125
// HTMLFrameElement implementation.
126
this._length = 0;
127
128
this._pretendToBeVisual = options.pretendToBeVisual;
131
// Some properties (such as localStorage and sessionStorage) share data
132
// between windows in the same origin. This object is intended
133
// to contain such data.
134
if (options.commonForOrigin && options.commonForOrigin[this._document.origin]) {
135
this._commonForOrigin = options.commonForOrigin;
136
} else {
137
this._commonForOrigin = {
138
[this._document.origin]: {
139
localStorageArea: new Map(),
140
sessionStorageArea: new Map(),
141
windowsInSameOrigin: [this]
142
}
143
};
144
}
145
146
this._currentOriginData = this._commonForOrigin[this._document.origin];
147
148
///// WEB STORAGE
149
150
this._localStorage = Storage.create([], {
151
associatedWindow: this,
152
storageArea: this._currentOriginData.localStorageArea,
153
type: "localStorage",
154
url: this._document.documentURI,
155
storageQuota: this._storageQuota
156
});
157
this._sessionStorage = Storage.create([], {
158
associatedWindow: this,
159
storageArea: this._currentOriginData.sessionStorageArea,
160
type: "sessionStorage",
161
url: this._document.documentURI,
162
storageQuota: this._storageQuota
167
const locationbar = BarProp.create();
168
const menubar = BarProp.create();
169
const personalbar = BarProp.create();
170
const scrollbars = BarProp.create();
171
const statusbar = BarProp.create();
172
const toolbar = BarProp.create();
173
const external = External.create();
174
const navigator = Navigator.create([], { userAgent: this._resourceLoader._userAgent });
175
const performance = Performance.create([], { rawPerformance });
178
define(this, {
179
get length() {
180
return window._length;
181
},
182
get window() {
183
return window._globalProxy;
184
},
186
return idlUtils.wrapperForImpl(window._frameElement);
188
get frames() {
189
return window._globalProxy;
190
},
191
get self() {
192
return window._globalProxy;
193
},
194
get parent() {
195
return window._parent;
196
},
197
get top() {
198
return window._top;
199
},
200
get document() {
201
return window._document;
202
},
203
get external() {
204
return external;
205
},
207
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location);
208
},
209
get history() {
210
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._history);
214
},
215
get locationbar() {
216
return locationbar;
217
},
218
get menubar() {
219
return menubar;
220
},
221
get personalbar() {
222
return personalbar;
223
},
224
get scrollbars() {
225
return scrollbars;
226
},
227
get statusbar() {
228
return statusbar;
229
},
230
get toolbar() {
231
return toolbar;
238
},
239
get localStorage() {
240
if (this._document.origin === "null") {
241
throw new DOMException("localStorage is not available for opaque origins", "SecurityError");
242
}
243
244
return this._localStorage;
245
},
246
get sessionStorage() {
247
if (this._document.origin === "null") {
248
throw new DOMException("sessionStorage is not available for opaque origins", "SecurityError");
249
}
250
251
return this._sessionStorage;
255
namedPropertiesWindow.initializeWindow(this, this._globalProxy);
257
///// METHODS for [ImplicitThis] hack
258
// See https://lists.w3.org/Archives/Public/public-script-coord/2015JanMar/0109.html
259
this.addEventListener = this.addEventListener.bind(this);
260
this.removeEventListener = this.removeEventListener.bind(this);
261
this.dispatchEvent = this.dispatchEvent.bind(this);
262
266
let latestAnimationFrameCallbackId = 0;
268
this.setTimeout = function (fn, ms) {
269
const args = [];
270
for (let i = 2; i < arguments.length; ++i) {
271
args[i - 2] = arguments[i];
272
}
273
return startTimer(window, setTimeout, clearTimeout, ++latestTimerId, fn, ms, timers, args);
274
};
275
this.setInterval = function (fn, ms) {
276
const args = [];
277
for (let i = 2; i < arguments.length; ++i) {
278
args[i - 2] = arguments[i];
279
}
280
return startTimer(window, setInterval, clearInterval, ++latestTimerId, fn, ms, timers, args);
281
};
282
this.clearInterval = stopTimer.bind(this, timers);
283
this.clearTimeout = stopTimer.bind(this, timers);
284
285
if (this._pretendToBeVisual) {
286
this.requestAnimationFrame = fn => {
287
const timestamp = rawPerformance.now() - windowInitialized;
288
const fps = 1000 / 60;
289
290
return startTimer(
291
window,
292
setTimeout,
293
clearTimeout,
294
++latestAnimationFrameCallbackId,
295
fn,
296
fps,
297
animationFrameCallbacks,
299
);
300
};
301
this.cancelAnimationFrame = stopTimer.bind(this, animationFrameCallbacks);
302
}
303
304
this.__stopAllTimers = function () {
305
stopAllTimers(timers);
306
stopAllTimers(animationFrameCallbacks);
307
308
latestTimerId = 0;
309
latestAnimationFrameCallbackId = 0;
310
311
timers = Object.create(null);
312
animationFrameCallbacks = Object.create(null);
315
function Option(text, value, defaultSelected, selected) {
316
if (text === undefined) {
317
text = "";
318
}
319
text = webIDLConversions.DOMString(text);
320
321
if (value !== undefined) {
322
value = webIDLConversions.DOMString(value);
323
}
324
325
defaultSelected = webIDLConversions.boolean(defaultSelected);
326
selected = webIDLConversions.boolean(selected);
327
328
const option = window._document.createElement("option");
329
const impl = idlUtils.implForWrapper(option);
330
334
if (value !== undefined) {
335
impl.setAttributeNS(null, "value", value);
337
if (defaultSelected) {
338
impl.setAttributeNS(null, "selected", "");
340
impl._selectedness = selected;
341
342
return option;
343
}
344
Object.defineProperty(Option, "prototype", {
345
value: this.HTMLOptionElement.prototype,
346
configurable: false,
347
enumerable: false,
348
writable: false
349
});
350
Object.defineProperty(window, "Option", {
351
value: Option,
352
configurable: true,
353
enumerable: false,
354
writable: true
355
});
356
357
function Image() {
358
const img = window._document.createElement("img");
359
const impl = idlUtils.implForWrapper(img);
360
361
if (arguments.length > 0) {
362
impl.setAttributeNS(null, "width", String(arguments[0]));
365
impl.setAttributeNS(null, "height", String(arguments[1]));
367
368
return img;
369
}
370
Object.defineProperty(Image, "prototype", {
371
value: this.HTMLImageElement.prototype,
372
configurable: false,
373
enumerable: false,
374
writable: false
375
});
376
Object.defineProperty(window, "Image", {
377
value: Image,
378
configurable: true,
379
enumerable: false,
380
writable: true
381
});
382
383
function Audio(src) {
384
const audio = window._document.createElement("audio");
385
const impl = idlUtils.implForWrapper(audio);
386
impl.setAttributeNS(null, "preload", "auto");
389
impl.setAttributeNS(null, "src", String(src));
390
}
391
392
return audio;
393
}
394
Object.defineProperty(Audio, "prototype", {
395
value: this.HTMLAudioElement.prototype,
396
configurable: false,
397
enumerable: false,
398
writable: false
399
});
400
Object.defineProperty(window, "Audio", {
401
value: Audio,
402
configurable: true,
403
enumerable: false,
404
writable: true
405
});
409
this.atob = function (str) {
410
const result = atob(str);
411
if (result === null) {
412
throw new DOMException("The string to be decoded contains invalid characters.", "InvalidCharacterError");
413
}
414
return result;
415
};
416
417
this.btoa = function (str) {
418
const result = btoa(str);
419
if (result === null) {
420
throw new DOMException("The string to be encoded contains invalid characters.", "InvalidCharacterError");
425
this.FileReader = createFileReader({
426
window: this
427
}).interface;
428
this.WebSocket = createWebSocket({
429
window: this
430
}).interface;
432
const AbortSignalWrapper = createAbortSignal({
433
window: this
434
});
435
this.AbortSignal = AbortSignalWrapper.interface;
436
this.AbortController = createAbortController({
437
AbortSignal: AbortSignalWrapper
438
}).interface;
439
440
this.XMLHttpRequest = createXMLHttpRequest(this);
441
442
// TODO: necessary for Blob and FileReader due to different-globals weirdness; investigate how to avoid this.
443
this.ArrayBuffer = ArrayBuffer;
444
this.Int8Array = Int8Array;
445
this.Uint8Array = Uint8Array;
446
this.Uint8ClampedArray = Uint8ClampedArray;
447
this.Int16Array = Int16Array;
448
this.Uint16Array = Uint16Array;
449
this.Int32Array = Int32Array;
450
this.Uint32Array = Uint32Array;
451
this.Float32Array = Float32Array;
452
this.Float64Array = Float64Array;
453
454
this.stop = function () {
455
const manager = idlUtils.implForWrapper(this._document)._requestManager;
456
if (manager) {
457
manager.close();
461
this.close = function () {
462
// Recursively close child frame windows, then ourselves.
463
const currentWindow = this;
464
(function windowCleaner(windowToClean) {
465
for (let i = 0; i < windowToClean.length; i++) {
466
windowCleaner(windowToClean[i]);
470
if (windowToClean !== currentWindow) {
471
windowToClean.close();
475
// Clear out all listeners. Any in-flight or upcoming events should not get delivered.
476
idlUtils.implForWrapper(this)._eventListeners = Object.create(null);
478
if (this._document) {
479
if (this._document.body) {
480
this._document.body.innerHTML = "";
484
// It's especially important to clear out the listeners here because document.close() causes a "load" event to
485
// fire.
486
idlUtils.implForWrapper(this._document)._eventListeners = Object.create(null);
489
const doc = idlUtils.implForWrapper(this._document);
490
if (doc._requestManager) {
491
doc._requestManager.close();
500
this.getComputedStyle = function (elt) {
501
elt = Element.convert(elt);
502
503
const declaration = new CSSStyleDeclaration();
504
const { forEach, indexOf } = Array.prototype;
505
const { style } = elt;
506
507
function setPropertiesFromRule(rule) {
508
if (!rule.selectorText) {
509
return;
510
}
511
512
const cssSelectorSplitRe = /((?:[^,"']|"[^"]*"|'[^']*')+)/;
513
const selectors = rule.selectorText.split(cssSelectorSplitRe);
516
for (const selectorText of selectors) {
517
if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(elt, selectorText)) {
519
forEach.call(rule.style, property => {
520
declaration.setProperty(
521
property,
522
rule.style.getPropertyValue(property),
523
rule.style.getPropertyPriority(property)
524
);
530
function readStylesFromStyleSheet(sheet) {
531
forEach.call(sheet.cssRules, rule => {
533
if (indexOf.call(rule.media, "screen") !== -1) {
534
forEach.call(rule.cssRules, setPropertiesFromRule);
535
}
536
} else {
537
setPropertiesFromRule(rule);
538
}
539
});
542
if (!parsedDefaultStyleSheet) {
543
parsedDefaultStyleSheet = cssom.parse(defaultStyleSheet);
544
}
545
readStylesFromStyleSheet(parsedDefaultStyleSheet);
546
forEach.call(elt.ownerDocument.styleSheets, readStylesFromStyleSheet);
548
forEach.call(style, property => {
549
declaration.setProperty(property, style.getPropertyValue(property), style.getPropertyPriority(property));
555
// The captureEvents() and releaseEvents() methods must do nothing
556
this.captureEvents = function () {};
557
558
this.releaseEvents = function () {};
559
560
///// PUBLIC DATA PROPERTIES (TODO: should be getters)
561
562
function wrapConsoleMethod(method) {
563
return (...args) => {
564
window._virtualConsole.emit(method, ...args);
565
};
566
}
567
568
this.console = {
569
assert: wrapConsoleMethod("assert"),
570
clear: wrapConsoleMethod("clear"),
571
count: wrapConsoleMethod("count"),
572
countReset: wrapConsoleMethod("countReset"),
573
debug: wrapConsoleMethod("debug"),
574
dir: wrapConsoleMethod("dir"),
575
dirxml: wrapConsoleMethod("dirxml"),
576
error: wrapConsoleMethod("error"),
577
group: wrapConsoleMethod("group"),
578
groupCollapsed: wrapConsoleMethod("groupCollapsed"),
579
groupEnd: wrapConsoleMethod("groupEnd"),
580
info: wrapConsoleMethod("info"),
581
log: wrapConsoleMethod("log"),
582
table: wrapConsoleMethod("table"),
583
time: wrapConsoleMethod("time"),
584
timeEnd: wrapConsoleMethod("timeEnd"),
585
trace: wrapConsoleMethod("trace"),
586
warn: wrapConsoleMethod("warn")
587
};
588
589
function notImplementedMethod(name) {
590
return function () {
591
notImplemented(name, window);
592
};
593
}
594
599
innerWidth: 1024,
600
innerHeight: 768,
601
outerWidth: 1024,
602
outerHeight: 768,
603
pageXOffset: 0,
604
pageYOffset: 0,
605
screenX: 0,
609
scrollX: 0,
610
scrollY: 0,
611
612
alert: notImplementedMethod("window.alert"),
613
blur: notImplementedMethod("window.blur"),
614
confirm: notImplementedMethod("window.confirm"),
615
focus: notImplementedMethod("window.focus"),
616
moveBy: notImplementedMethod("window.moveBy"),
617
moveTo: notImplementedMethod("window.moveTo"),
618
open: notImplementedMethod("window.open"),
619
print: notImplementedMethod("window.print"),
620
prompt: notImplementedMethod("window.prompt"),
621
resizeBy: notImplementedMethod("window.resizeBy"),
622
resizeTo: notImplementedMethod("window.resizeTo"),
623
scroll: notImplementedMethod("window.scroll"),
624
scrollBy: notImplementedMethod("window.scrollBy"),
625
scrollTo: notImplementedMethod("window.scrollTo")
626
});
627
628
///// INITIALIZATION
629
630
process.nextTick(() => {
631
if (!window.document) {
632
return; // window might've been closed already
633
}
634
635
if (window.document.readyState === "complete") {
636
fireAnEvent("load", window, undefined, {}, window.document);
638
window.document.addEventListener("load", () => {
639
fireAnEvent("load", window, undefined, {}, window.document);
640
641
if (!idlUtils.implForWrapper(window._document)._pageShowingFlag) {
642
idlUtils.implForWrapper(window._document)._pageShowingFlag = true;
643
fireAnEvent("pageshow", window, PageTransitionEvent, { persisted: false }, window.document);
644
}
650
Object.setPrototypeOf(Window, EventTarget.interface);
651
Object.setPrototypeOf(Window.prototype, EventTarget.interface.prototype);
652
Object.defineProperty(Window.prototype, Symbol.toStringTag, {
653
value: "Window",
654
writable: false,
655
enumerable: false,
656
configurable: true
657
});
659
function startTimer(window, startFn, stopFn, timerId, callback, ms, timerStorage, args) {
660
if (!window || !window._document) {
661
return undefined;
662
}
663
if (typeof callback !== "function") {
664
const code = String(callback);
665
callback = window._globalProxy.eval.bind(window, code + `\n//# sourceURL=${window.location.href}`);
666
}
667
668
const oldCallback = callback;
669
callback = () => {
670
try {
671
oldCallback.apply(window._globalProxy, args);
672
} catch (e) {
673
reportException(window, e, window.location.href);
674
}
675
};
676
677
const res = startFn(callback, ms);
678
timerStorage[timerId] = [res, stopFn];
682
function stopTimer(timerStorage, id) {
683
const timer = timerStorage[id];
685
// Need to .call() with undefined to ensure the thisArg is not timer itself
686
timer[1].call(undefined, timer[0]);
691
function stopAllTimers(timers) {
692
Object.keys(timers).forEach(key => {
693
const timer = timers[key];
694
// Need to .call() with undefined to ensure the thisArg is not timer itself
695
timer[1].call(undefined, timer[0]);
698
699
function contextifyWindow(window) {
700
if (vm.isContext(window)) {
701
return;
702
}
703
704
vm.createContext(window);
705
const documentImpl = idlUtils.implForWrapper(window._document);
706
documentImpl._defaultView = window._globalProxy = vm.runInContext("this", window);
707
}