Permalink
Cannot retrieve contributors at this time
| "use strict"; | |
| const vm = require("vm"); | |
| const webIDLConversions = require("webidl-conversions"); | |
| const { CSSStyleDeclaration } = require("cssstyle"); | |
| const { Performance: RawPerformance } = require("w3c-hr-time"); | |
| const notImplemented = require("./not-implemented"); | |
| const { define, mixin } = require("../utils"); | |
| const Element = require("../living/generated/Element"); | |
| const EventTarget = require("../living/generated/EventTarget"); | |
| const PageTransitionEvent = require("../living/generated/PageTransitionEvent"); | |
| const namedPropertiesWindow = require("../living/named-properties-window"); | |
| const cssom = require("cssom"); | |
| const postMessage = require("../living/post-message"); | |
| const DOMException = require("domexception"); | |
| const { btoa, atob } = require("abab"); | |
| const idlUtils = require("../living/generated/utils"); | |
| const createXMLHttpRequest = require("../living/xmlhttprequest"); | |
| const createFileReader = require("../living/generated/FileReader").createInterface; | |
| const createWebSocket = require("../living/generated/WebSocket").createInterface; | |
| const WebSocketImpl = require("../living/websockets/WebSocket-impl").implementation; | |
| const BarProp = require("../living/generated/BarProp"); | |
| const Document = require("../living/generated/Document"); | |
| const External = require("../living/generated/External"); | |
| const Navigator = require("../living/generated/Navigator"); | |
| const Performance = require("../living/generated/Performance"); | |
| const Screen = require("../living/generated/Screen"); | |
| const Storage = require("../living/generated/Storage"); | |
| const createAbortController = require("../living/generated/AbortController").createInterface; | |
| const createAbortSignal = require("../living/generated/AbortSignal").createInterface; | |
| const reportException = require("../living/helpers/runtime-script-errors"); | |
| const { matchesDontThrow } = require("../living/helpers/selectors"); | |
| const { fireAnEvent } = require("../living/helpers/events"); | |
| const SessionHistory = require("../living/window/SessionHistory"); | |
| const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation; | |
| const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation; | |
| const defaultStyleSheet = require("./default-stylesheet"); | |
| let parsedDefaultStyleSheet; | |
| // NB: the require() must be after assigning `module.exports` because this require() is circular | |
| // TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up. | |
| module.exports = Window; | |
| const dom = require("../living"); | |
| dom.Window = Window; | |
| // NOTE: per https://heycam.github.io/webidl/#Global, all properties on the Window object must be own-properties. | |
| // That is why we assign everything inside of the constructor, instead of using a shared prototype. | |
| // You can verify this in e.g. Firefox or Internet Explorer, which do a good job with Web IDL compliance. | |
| function Window(options) { | |
| EventTarget.setup(this); | |
| const rawPerformance = new RawPerformance(); | |
| const windowInitialized = rawPerformance.now(); | |
| const window = this; | |
| mixin(window, WindowEventHandlersImpl.prototype); | |
| mixin(window, GlobalEventHandlersImpl.prototype); | |
| this._initGlobalEvents(); | |
| ///// INTERFACES FROM THE DOM | |
| // TODO: consider a mode of some sort where these are not shared between all DOM instances | |
| // It'd be very memory-expensive in most cases, though. | |
| for (const name in dom) { | |
| Object.defineProperty(window, name, { | |
| enumerable: false, | |
| configurable: true, | |
| writable: true, | |
| value: dom[name] | |
| }); | |
| } | |
| ///// PRIVATE DATA PROPERTIES | |
| this._resourceLoader = options.resourceLoader; | |
| // vm initialization is deferred until script processing is activated | |
| this._globalProxy = this; | |
| Object.defineProperty(idlUtils.implForWrapper(this), idlUtils.wrapperSymbol, { get: () => this._globalProxy }); | |
| let timers = Object.create(null); | |
| let animationFrameCallbacks = Object.create(null); | |
| // List options explicitly to be clear which are passed through | |
| this._document = Document.create([], { | |
| options: { | |
| parsingMode: options.parsingMode, | |
| contentType: options.contentType, | |
| encoding: options.encoding, | |
| cookieJar: options.cookieJar, | |
| url: options.url, | |
| lastModified: options.lastModified, | |
| referrer: options.referrer, | |
| concurrentNodeIterators: options.concurrentNodeIterators, | |
| parseOptions: options.parseOptions, | |
| defaultView: this._globalProxy, | |
| global: this | |
| } | |
| }); | |
| // https://html.spec.whatwg.org/#session-history | |
| this._sessionHistory = new SessionHistory({ | |
| document: idlUtils.implForWrapper(this._document), | |
| url: idlUtils.implForWrapper(this._document)._URL, | |
| stateObject: null | |
| }, this); | |
| this._virtualConsole = options.virtualConsole; | |
| this._runScripts = options.runScripts; | |
| if (this._runScripts === "outside-only" || this._runScripts === "dangerously") { | |
| contextifyWindow(this); | |
| } | |
| // Set up the window as if it's a top level window. | |
| // If it's not, then references will be corrected by frame/iframe code. | |
| this._parent = this._top = this._globalProxy; | |
| this._frameElement = null; | |
| // This implements window.frames.length, since window.frames returns a | |
| // self reference to the window object. This value is incremented in the | |
| // HTMLFrameElement implementation. | |
| this._length = 0; | |
| this._pretendToBeVisual = options.pretendToBeVisual; | |
| this._storageQuota = options.storageQuota; | |
| // Some properties (such as localStorage and sessionStorage) share data | |
| // between windows in the same origin. This object is intended | |
| // to contain such data. | |
| if (options.commonForOrigin && options.commonForOrigin[this._document.origin]) { | |
| this._commonForOrigin = options.commonForOrigin; | |
| } else { | |
| this._commonForOrigin = { | |
| [this._document.origin]: { | |
| localStorageArea: new Map(), | |
| sessionStorageArea: new Map(), | |
| windowsInSameOrigin: [this] | |
| } | |
| }; | |
| } | |
| this._currentOriginData = this._commonForOrigin[this._document.origin]; | |
| ///// WEB STORAGE | |
| this._localStorage = Storage.create([], { | |
| associatedWindow: this, | |
| storageArea: this._currentOriginData.localStorageArea, | |
| type: "localStorage", | |
| url: this._document.documentURI, | |
| storageQuota: this._storageQuota | |
| }); | |
| this._sessionStorage = Storage.create([], { | |
| associatedWindow: this, | |
| storageArea: this._currentOriginData.sessionStorageArea, | |
| type: "sessionStorage", | |
| url: this._document.documentURI, | |
| storageQuota: this._storageQuota | |
| }); | |
| ///// GETTERS | |
| const locationbar = BarProp.create(); | |
| const menubar = BarProp.create(); | |
| const personalbar = BarProp.create(); | |
| const scrollbars = BarProp.create(); | |
| const statusbar = BarProp.create(); | |
| const toolbar = BarProp.create(); | |
| const external = External.create(); | |
| const navigator = Navigator.create([], { userAgent: this._resourceLoader._userAgent }); | |
| const performance = Performance.create([], { rawPerformance }); | |
| const screen = Screen.create(); | |
| define(this, { | |
| get length() { | |
| return window._length; | |
| }, | |
| get window() { | |
| return window._globalProxy; | |
| }, | |
| get frameElement() { | |
| return idlUtils.wrapperForImpl(window._frameElement); | |
| }, | |
| get frames() { | |
| return window._globalProxy; | |
| }, | |
| get self() { | |
| return window._globalProxy; | |
| }, | |
| get parent() { | |
| return window._parent; | |
| }, | |
| get top() { | |
| return window._top; | |
| }, | |
| get document() { | |
| return window._document; | |
| }, | |
| get external() { | |
| return external; | |
| }, | |
| get location() { | |
| return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location); | |
| }, | |
| get history() { | |
| return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._history); | |
| }, | |
| get navigator() { | |
| return navigator; | |
| }, | |
| get locationbar() { | |
| return locationbar; | |
| }, | |
| get menubar() { | |
| return menubar; | |
| }, | |
| get personalbar() { | |
| return personalbar; | |
| }, | |
| get scrollbars() { | |
| return scrollbars; | |
| }, | |
| get statusbar() { | |
| return statusbar; | |
| }, | |
| get toolbar() { | |
| return toolbar; | |
| }, | |
| get performance() { | |
| return performance; | |
| }, | |
| get screen() { | |
| return screen; | |
| }, | |
| get localStorage() { | |
| if (this._document.origin === "null") { | |
| throw new DOMException("localStorage is not available for opaque origins", "SecurityError"); | |
| } | |
| return this._localStorage; | |
| }, | |
| get sessionStorage() { | |
| if (this._document.origin === "null") { | |
| throw new DOMException("sessionStorage is not available for opaque origins", "SecurityError"); | |
| } | |
| return this._sessionStorage; | |
| } | |
| }); | |
| namedPropertiesWindow.initializeWindow(this, this._globalProxy); | |
| ///// METHODS for [ImplicitThis] hack | |
| // See https://lists.w3.org/Archives/Public/public-script-coord/2015JanMar/0109.html | |
| this.addEventListener = this.addEventListener.bind(this); | |
| this.removeEventListener = this.removeEventListener.bind(this); | |
| this.dispatchEvent = this.dispatchEvent.bind(this); | |
| ///// METHODS | |
| let latestTimerId = 0; | |
| let latestAnimationFrameCallbackId = 0; | |
| this.setTimeout = function (fn, ms) { | |
| const args = []; | |
| for (let i = 2; i < arguments.length; ++i) { | |
| args[i - 2] = arguments[i]; | |
| } | |
| return startTimer(window, setTimeout, clearTimeout, ++latestTimerId, fn, ms, timers, args); | |
| }; | |
| this.setInterval = function (fn, ms) { | |
| const args = []; | |
| for (let i = 2; i < arguments.length; ++i) { | |
| args[i - 2] = arguments[i]; | |
| } | |
| return startTimer(window, setInterval, clearInterval, ++latestTimerId, fn, ms, timers, args); | |
| }; | |
| this.clearInterval = stopTimer.bind(this, timers); | |
| this.clearTimeout = stopTimer.bind(this, timers); | |
| if (this._pretendToBeVisual) { | |
| this.requestAnimationFrame = fn => { | |
| const timestamp = rawPerformance.now() - windowInitialized; | |
| const fps = 1000 / 60; | |
| return startTimer( | |
| window, | |
| setTimeout, | |
| clearTimeout, | |
| ++latestAnimationFrameCallbackId, | |
| fn, | |
| fps, | |
| animationFrameCallbacks, | |
| [timestamp] | |
| ); | |
| }; | |
| this.cancelAnimationFrame = stopTimer.bind(this, animationFrameCallbacks); | |
| } | |
| this.__stopAllTimers = function () { | |
| stopAllTimers(timers); | |
| stopAllTimers(animationFrameCallbacks); | |
| latestTimerId = 0; | |
| latestAnimationFrameCallbackId = 0; | |
| timers = Object.create(null); | |
| animationFrameCallbacks = Object.create(null); | |
| }; | |
| function Option(text, value, defaultSelected, selected) { | |
| if (text === undefined) { | |
| text = ""; | |
| } | |
| text = webIDLConversions.DOMString(text); | |
| if (value !== undefined) { | |
| value = webIDLConversions.DOMString(value); | |
| } | |
| defaultSelected = webIDLConversions.boolean(defaultSelected); | |
| selected = webIDLConversions.boolean(selected); | |
| const option = window._document.createElement("option"); | |
| const impl = idlUtils.implForWrapper(option); | |
| if (text !== "") { | |
| impl.text = text; | |
| } | |
| if (value !== undefined) { | |
| impl.setAttributeNS(null, "value", value); | |
| } | |
| if (defaultSelected) { | |
| impl.setAttributeNS(null, "selected", ""); | |
| } | |
| impl._selectedness = selected; | |
| return option; | |
| } | |
| Object.defineProperty(Option, "prototype", { | |
| value: this.HTMLOptionElement.prototype, | |
| configurable: false, | |
| enumerable: false, | |
| writable: false | |
| }); | |
| Object.defineProperty(window, "Option", { | |
| value: Option, | |
| configurable: true, | |
| enumerable: false, | |
| writable: true | |
| }); | |
| function Image() { | |
| const img = window._document.createElement("img"); | |
| const impl = idlUtils.implForWrapper(img); | |
| if (arguments.length > 0) { | |
| impl.setAttributeNS(null, "width", String(arguments[0])); | |
| } | |
| if (arguments.length > 1) { | |
| impl.setAttributeNS(null, "height", String(arguments[1])); | |
| } | |
| return img; | |
| } | |
| Object.defineProperty(Image, "prototype", { | |
| value: this.HTMLImageElement.prototype, | |
| configurable: false, | |
| enumerable: false, | |
| writable: false | |
| }); | |
| Object.defineProperty(window, "Image", { | |
| value: Image, | |
| configurable: true, | |
| enumerable: false, | |
| writable: true | |
| }); | |
| function Audio(src) { | |
| const audio = window._document.createElement("audio"); | |
| const impl = idlUtils.implForWrapper(audio); | |
| impl.setAttributeNS(null, "preload", "auto"); | |
| if (src !== undefined) { | |
| impl.setAttributeNS(null, "src", String(src)); | |
| } | |
| return audio; | |
| } | |
| Object.defineProperty(Audio, "prototype", { | |
| value: this.HTMLAudioElement.prototype, | |
| configurable: false, | |
| enumerable: false, | |
| writable: false | |
| }); | |
| Object.defineProperty(window, "Audio", { | |
| value: Audio, | |
| configurable: true, | |
| enumerable: false, | |
| writable: true | |
| }); | |
| this.postMessage = postMessage; | |
| this.atob = function (str) { | |
| const result = atob(str); | |
| if (result === null) { | |
| throw new DOMException("The string to be decoded contains invalid characters.", "InvalidCharacterError"); | |
| } | |
| return result; | |
| }; | |
| this.btoa = function (str) { | |
| const result = btoa(str); | |
| if (result === null) { | |
| throw new DOMException("The string to be encoded contains invalid characters.", "InvalidCharacterError"); | |
| } | |
| return result; | |
| }; | |
| this.FileReader = createFileReader({ | |
| window: this | |
| }).interface; | |
| this.WebSocket = createWebSocket({ | |
| window: this | |
| }).interface; | |
| const AbortSignalWrapper = createAbortSignal({ | |
| window: this | |
| }); | |
| this.AbortSignal = AbortSignalWrapper.interface; | |
| this.AbortController = createAbortController({ | |
| AbortSignal: AbortSignalWrapper | |
| }).interface; | |
| this.XMLHttpRequest = createXMLHttpRequest(this); | |
| // TODO: necessary for Blob and FileReader due to different-globals weirdness; investigate how to avoid this. | |
| this.ArrayBuffer = ArrayBuffer; | |
| this.Int8Array = Int8Array; | |
| this.Uint8Array = Uint8Array; | |
| this.Uint8ClampedArray = Uint8ClampedArray; | |
| this.Int16Array = Int16Array; | |
| this.Uint16Array = Uint16Array; | |
| this.Int32Array = Int32Array; | |
| this.Uint32Array = Uint32Array; | |
| this.Float32Array = Float32Array; | |
| this.Float64Array = Float64Array; | |
| this.stop = function () { | |
| const manager = idlUtils.implForWrapper(this._document)._requestManager; | |
| if (manager) { | |
| manager.close(); | |
| } | |
| }; | |
| this.close = function () { | |
| // Recursively close child frame windows, then ourselves. | |
| const currentWindow = this; | |
| (function windowCleaner(windowToClean) { | |
| for (let i = 0; i < windowToClean.length; i++) { | |
| windowCleaner(windowToClean[i]); | |
| } | |
| // We"re already in our own window.close(). | |
| if (windowToClean !== currentWindow) { | |
| windowToClean.close(); | |
| } | |
| }(this)); | |
| // Clear out all listeners. Any in-flight or upcoming events should not get delivered. | |
| idlUtils.implForWrapper(this)._eventListeners = Object.create(null); | |
| if (this._document) { | |
| if (this._document.body) { | |
| this._document.body.innerHTML = ""; | |
| } | |
| if (this._document.close) { | |
| // It's especially important to clear out the listeners here because document.close() causes a "load" event to | |
| // fire. | |
| idlUtils.implForWrapper(this._document)._eventListeners = Object.create(null); | |
| this._document.close(); | |
| } | |
| const doc = idlUtils.implForWrapper(this._document); | |
| if (doc._requestManager) { | |
| doc._requestManager.close(); | |
| } | |
| delete this._document; | |
| } | |
| this.__stopAllTimers(); | |
| WebSocketImpl.cleanUpWindow(this); | |
| }; | |
| this.getComputedStyle = function (elt) { | |
| elt = Element.convert(elt); | |
| const declaration = new CSSStyleDeclaration(); | |
| const { forEach, indexOf } = Array.prototype; | |
| const { style } = elt; | |
| function setPropertiesFromRule(rule) { | |
| if (!rule.selectorText) { | |
| return; | |
| } | |
| const cssSelectorSplitRe = /((?:[^,"']|"[^"]*"|'[^']*')+)/; | |
| const selectors = rule.selectorText.split(cssSelectorSplitRe); | |
| let matched = false; | |
| for (const selectorText of selectors) { | |
| if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(elt, selectorText)) { | |
| matched = true; | |
| forEach.call(rule.style, property => { | |
| declaration.setProperty( | |
| property, | |
| rule.style.getPropertyValue(property), | |
| rule.style.getPropertyPriority(property) | |
| ); | |
| }); | |
| } | |
| } | |
| } | |
| function readStylesFromStyleSheet(sheet) { | |
| forEach.call(sheet.cssRules, rule => { | |
| if (rule.media) { | |
| if (indexOf.call(rule.media, "screen") !== -1) { | |
| forEach.call(rule.cssRules, setPropertiesFromRule); | |
| } | |
| } else { | |
| setPropertiesFromRule(rule); | |
| } | |
| }); | |
| } | |
| if (!parsedDefaultStyleSheet) { | |
| parsedDefaultStyleSheet = cssom.parse(defaultStyleSheet); | |
| } | |
| readStylesFromStyleSheet(parsedDefaultStyleSheet); | |
| forEach.call(elt.ownerDocument.styleSheets, readStylesFromStyleSheet); | |
| forEach.call(style, property => { | |
| declaration.setProperty(property, style.getPropertyValue(property), style.getPropertyPriority(property)); | |
| }); | |
| return declaration; | |
| }; | |
| // The captureEvents() and releaseEvents() methods must do nothing | |
| this.captureEvents = function () {}; | |
| this.releaseEvents = function () {}; | |
| ///// PUBLIC DATA PROPERTIES (TODO: should be getters) | |
| function wrapConsoleMethod(method) { | |
| return (...args) => { | |
| window._virtualConsole.emit(method, ...args); | |
| }; | |
| } | |
| this.console = { | |
| assert: wrapConsoleMethod("assert"), | |
| clear: wrapConsoleMethod("clear"), | |
| count: wrapConsoleMethod("count"), | |
| countReset: wrapConsoleMethod("countReset"), | |
| debug: wrapConsoleMethod("debug"), | |
| dir: wrapConsoleMethod("dir"), | |
| dirxml: wrapConsoleMethod("dirxml"), | |
| error: wrapConsoleMethod("error"), | |
| group: wrapConsoleMethod("group"), | |
| groupCollapsed: wrapConsoleMethod("groupCollapsed"), | |
| groupEnd: wrapConsoleMethod("groupEnd"), | |
| info: wrapConsoleMethod("info"), | |
| log: wrapConsoleMethod("log"), | |
| table: wrapConsoleMethod("table"), | |
| time: wrapConsoleMethod("time"), | |
| timeEnd: wrapConsoleMethod("timeEnd"), | |
| trace: wrapConsoleMethod("trace"), | |
| warn: wrapConsoleMethod("warn") | |
| }; | |
| function notImplementedMethod(name) { | |
| return function () { | |
| notImplemented(name, window); | |
| }; | |
| } | |
| define(this, { | |
| name: "", | |
| status: "", | |
| devicePixelRatio: 1, | |
| innerWidth: 1024, | |
| innerHeight: 768, | |
| outerWidth: 1024, | |
| outerHeight: 768, | |
| pageXOffset: 0, | |
| pageYOffset: 0, | |
| screenX: 0, | |
| screenLeft: 0, | |
| screenY: 0, | |
| screenTop: 0, | |
| scrollX: 0, | |
| scrollY: 0, | |
| alert: notImplementedMethod("window.alert"), | |
| blur: notImplementedMethod("window.blur"), | |
| confirm: notImplementedMethod("window.confirm"), | |
| focus: notImplementedMethod("window.focus"), | |
| moveBy: notImplementedMethod("window.moveBy"), | |
| moveTo: notImplementedMethod("window.moveTo"), | |
| open: notImplementedMethod("window.open"), | |
| print: notImplementedMethod("window.print"), | |
| prompt: notImplementedMethod("window.prompt"), | |
| resizeBy: notImplementedMethod("window.resizeBy"), | |
| resizeTo: notImplementedMethod("window.resizeTo"), | |
| scroll: notImplementedMethod("window.scroll"), | |
| scrollBy: notImplementedMethod("window.scrollBy"), | |
| scrollTo: notImplementedMethod("window.scrollTo") | |
| }); | |
| ///// INITIALIZATION | |
| process.nextTick(() => { | |
| if (!window.document) { | |
| return; // window might've been closed already | |
| } | |
| if (window.document.readyState === "complete") { | |
| fireAnEvent("load", window, undefined, {}, window.document); | |
| } else { | |
| window.document.addEventListener("load", () => { | |
| fireAnEvent("load", window, undefined, {}, window.document); | |
| if (!idlUtils.implForWrapper(window._document)._pageShowingFlag) { | |
| idlUtils.implForWrapper(window._document)._pageShowingFlag = true; | |
| fireAnEvent("pageshow", window, PageTransitionEvent, { persisted: false }, window.document); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| Object.setPrototypeOf(Window, EventTarget.interface); | |
| Object.setPrototypeOf(Window.prototype, EventTarget.interface.prototype); | |
| Object.defineProperty(Window.prototype, Symbol.toStringTag, { | |
| value: "Window", | |
| writable: false, | |
| enumerable: false, | |
| configurable: true | |
| }); | |
| function startTimer(window, startFn, stopFn, timerId, callback, ms, timerStorage, args) { | |
| if (!window || !window._document) { | |
| return undefined; | |
| } | |
| if (typeof callback !== "function") { | |
| const code = String(callback); | |
| callback = window._globalProxy.eval.bind(window, code + `\n//# sourceURL=${window.location.href}`); | |
| } | |
| const oldCallback = callback; | |
| callback = () => { | |
| try { | |
| oldCallback.apply(window._globalProxy, args); | |
| } catch (e) { | |
| reportException(window, e, window.location.href); | |
| } | |
| }; | |
| const res = startFn(callback, ms); | |
| timerStorage[timerId] = [res, stopFn]; | |
| return timerId; | |
| } | |
| function stopTimer(timerStorage, id) { | |
| const timer = timerStorage[id]; | |
| if (timer) { | |
| // Need to .call() with undefined to ensure the thisArg is not timer itself | |
| timer[1].call(undefined, timer[0]); | |
| delete timerStorage[id]; | |
| } | |
| } | |
| function stopAllTimers(timers) { | |
| Object.keys(timers).forEach(key => { | |
| const timer = timers[key]; | |
| // Need to .call() with undefined to ensure the thisArg is not timer itself | |
| timer[1].call(undefined, timer[0]); | |
| }); | |
| } | |
| function contextifyWindow(window) { | |
| if (vm.isContext(window)) { | |
| return; | |
| } | |
| vm.createContext(window); | |
| const documentImpl = idlUtils.implForWrapper(window._document); | |
| documentImpl._defaultView = window._globalProxy = vm.runInContext("this", window); | |
| } |