Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
707 lines (612 sloc)
21.4 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "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); | |
| } |