"use strict"; const CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration; const notImplemented = require("./not-implemented"); const VirtualConsole = require("../virtual-console"); const define = require("../utils").define; const EventTarget = require("../living/generated/EventTarget"); const namedPropertiesWindow = require("../living/named-properties-window"); const cssom = require("cssom"); const postMessage = require("../living/post-message"); const DOMException = require("../web-idl/DOMException"); const btoa = require("abab").btoa; const atob = require("abab").atob; const idlUtils = require("../living/generated/utils"); const createFileReader = require("../living/file-reader"); const createXMLHttpRequest = require("../living/xmlhttprequest"); const Document = require("../living/generated/Document"); const Navigator = require("../living/generated/Navigator"); const reportException = require("../living/helpers/runtime-script-errors"); // 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"); const cssSelectorSplitRE = /((?:[^,"']|"[^"]*"|'[^']*')+)/; const defaultStyleSheet = cssom.parse(require("./default-stylesheet")); 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 window = this; ///// 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] }); } this._core = dom; ///// PRIVATE DATA PROPERTIES // vm initialization is defered until script processing is activated (in level1/core) this._globalProxy = this; this.__timers = Object.create(null); // 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; // List options explicitly to be clear which are passed through this._document = Document.create([], { core: dom, options: { parsingMode: options.parsingMode, contentType: options.contentType, encoding: options.encoding, cookieJar: options.cookieJar, parser: options.parser, url: options.url, lastModified: options.lastModified, referrer: options.referrer, cookie: options.cookie, deferClose: options.deferClose, resourceLoader: options.resourceLoader, concurrentNodeIterators: options.concurrentNodeIterators, pool: options.pool, agent: options.agent, agentClass: options.agentClass, agentOptions: options.agentOptions, strictSSL: options.strictSSL, proxy: options.proxy, defaultView: this._globalProxy, global: this } }); // https://html.spec.whatwg.org/#session-history this._sessionHistory = [{ document: idlUtils.implForWrapper(this._document), url: idlUtils.implForWrapper(this._document)._URL, stateObject: null }]; this._currentSessionHistoryEntryIndex = 0; // This implements window.frames.length, since window.frames returns a // self reference to the window object. This value is incremented in the // HTMLFrameElement init function (see: level2/html.js). this._length = 0; if (options.virtualConsole) { if (options.virtualConsole instanceof VirtualConsole) { this._virtualConsole = options.virtualConsole; } else { throw new TypeError( "options.virtualConsole must be a VirtualConsole (from createVirtualConsole)"); } } else { this._virtualConsole = new VirtualConsole(); } ///// GETTERS const navigator = Navigator.create([], { userAgent: options.userAgent }); define(this, { get length() { return window._length; }, get window() { return window._globalProxy; }, get frameElement() { return 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 location() { return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location); }, get history() { return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._history); }, get navigator() { return navigator; } }); namedPropertiesWindow.initializeWindow(this, dom.HTMLCollection); ///// 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; 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, 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, args); }; this.clearInterval = stopTimer.bind(this, window); this.clearTimeout = stopTimer.bind(this, window); this.__stopAllTimers = stopAllTimers.bind(this, window); function Image() { const img = window._document.createElement("img"); const impl = idlUtils.implForWrapper(img); if (arguments.length > 0) { impl.setAttribute("width", String(arguments[0])); } if (arguments.length > 1) { impl.setAttribute("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.setAttribute("preload", "auto"); if (src !== undefined) { impl.setAttribute("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 }); function wrapConsoleMethod(method) { return function () { const args = Array.prototype.slice.call(arguments); window._virtualConsole.emit.apply(window._virtualConsole, [method].concat(args)); }; } this.postMessage = postMessage; this.atob = function (str) { const result = atob(str); if (result === null) { throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "The string to be decoded contains invalid characters."); } return result; }; this.btoa = function (str) { const result = btoa(str); if (result === null) { throw new DOMException(DOMException.INVALID_CHARACTER_ERR, "The string to be encoded contains invalid characters."); } return result; }; this.FileReader = createFileReader(this); 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; } stopAllTimers(currentWindow); }; this.getComputedStyle = function (node) { const s = node.style; const cs = new CSSStyleDeclaration(); const forEach = Array.prototype.forEach; function setPropertiesFromRule(rule) { if (!rule.selectorText) { return; } const selectors = rule.selectorText.split(cssSelectorSplitRE); let matched = false; for (const selectorText of selectors) { if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(node, selectorText)) { matched = true; forEach.call(rule.style, property => { cs.setProperty(property, rule.style.getPropertyValue(property), rule.style.getPropertyPriority(property)); }); } } } function readStylesFromStyleSheet(sheet) { forEach.call(sheet.cssRules, rule => { if (rule.media) { if (Array.prototype.indexOf.call(rule.media, "screen") !== -1) { forEach.call(rule.cssRules, setPropertiesFromRule); } } else { setPropertiesFromRule(rule); } }); } readStylesFromStyleSheet(defaultStyleSheet); forEach.call(node.ownerDocument.styleSheets, readStylesFromStyleSheet); forEach.call(s, property => { cs.setProperty(property, s.getPropertyValue(property), s.getPropertyPriority(property)); }); return cs; }; ///// PUBLIC DATA PROPERTIES (TODO: should be getters) this.console = { assert: wrapConsoleMethod("assert"), clear: wrapConsoleMethod("clear"), count: wrapConsoleMethod("count"), debug: wrapConsoleMethod("debug"), 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: "nodejs", innerWidth: 1024, innerHeight: 768, outerWidth: 1024, outerHeight: 768, pageXOffset: 0, pageYOffset: 0, screenX: 0, screenY: 0, screenLeft: 0, screenTop: 0, scrollX: 0, scrollY: 0, scrollTop: 0, scrollLeft: 0, screen: { width: 0, height: 0 }, alert: notImplementedMethod("window.alert"), blur: notImplementedMethod("window.blur"), confirm: notImplementedMethod("window.confirm"), createPopup: notImplementedMethod("window.createPopup"), 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"), toString: () => { return "[object Window]"; } }); ///// INITIALIZATION process.nextTick(() => { if (!window.document) { return; // window might've been closed already } if (window.document.readyState === "complete") { const ev = window.document.createEvent("HTMLEvents"); ev.initEvent("load", false, false); window.dispatchEvent(ev); } else { window.document.addEventListener("load", () => { const ev = window.document.createEvent("HTMLEvents"); ev.initEvent("load", false, false); window.dispatchEvent(ev); }); } }); } Object.setPrototypeOf(Window, EventTarget.interface); Object.setPrototypeOf(Window.prototype, EventTarget.interface.prototype); function matchesDontThrow(el, selector) { try { return el.matches(selector); } catch (e) { return false; } } function startTimer(window, startFn, stopFn, timerId, callback, ms, args) { 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); window.__timers[timerId] = [res, stopFn]; return timerId; } function stopTimer(window, id) { const timer = window.__timers[id]; if (timer) { timer[1].call(window, timer[0]); delete window.__timers[id]; } } function stopAllTimers(window) { Object.keys(window.__timers).forEach(key => { const timer = window.__timers[key]; timer[1].call(window, timer[0]); }); window.__timers = Object.create(null); }