import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal'; import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils'; import { internalContextBridge } from '@electron/internal/renderer/api/context-bridge'; import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages'; const { contextIsolationEnabled } = internalContextBridge; // This file implements the following APIs over the ctx bridge: // - window.open() // - window.opener.blur() // - window.opener.close() // - window.opener.eval() // - window.opener.focus() // - window.opener.location // - window.opener.print() // - window.opener.closed // - window.opener.postMessage() // - window.history.back() // - window.history.forward() // - window.history.go() // - window.history.length // - window.prompt() // - document.hidden // - document.visibilityState // Helper function to resolve relative url. const resolveURL = (url: string, base: string) => new URL(url, base).href; // Use this method to ensure values expected as strings in the main process // are convertible to strings in the renderer process. This ensures exceptions // converting values to strings are thrown in this process. const toString = (value: any) => { return value != null ? `${value}` : value; }; const windowProxies = new Map(); const getOrCreateProxy = (guestId: number): SafelyBoundBrowserWindowProxy => { let proxy = windowProxies.get(guestId); if (proxy == null) { proxy = new BrowserWindowProxy(guestId); windowProxies.set(guestId, proxy); } return proxy.getSafe(); }; const removeProxy = (guestId: number) => { windowProxies.delete(guestId); }; type LocationProperties = 'hash' | 'href' | 'host' | 'hostname' | 'origin' | 'pathname' | 'port' | 'protocol' | 'search' class LocationProxy { @LocationProxy.ProxyProperty public hash!: string; @LocationProxy.ProxyProperty public href!: string; @LocationProxy.ProxyProperty public host!: string; @LocationProxy.ProxyProperty public hostname!: string; @LocationProxy.ProxyProperty public origin!: string; @LocationProxy.ProxyProperty public pathname!: string; @LocationProxy.ProxyProperty public port!: string; @LocationProxy.ProxyProperty public protocol!: string; @LocationProxy.ProxyProperty public search!: URLSearchParams; private guestId: number; /** * Beware: This decorator will have the _prototype_ as the `target`. It defines properties * commonly found in URL on the LocationProxy. */ private static ProxyProperty (target: LocationProxy, propertyKey: LocationProperties) { Object.defineProperty(target, propertyKey, { enumerable: true, configurable: true, get: function (this: LocationProxy): T | string { const guestURL = this.getGuestURL(); const value = guestURL ? guestURL[propertyKey] : ''; return value === undefined ? '' : value; }, set: function (this: LocationProxy, newVal: T) { const guestURL = this.getGuestURL(); if (guestURL) { // TypeScript doesn't want us to assign to read-only variables. // It's right, that's bad, but we're doing it anyway. (guestURL as any)[propertyKey] = newVal; return this._invokeWebContentsMethod('loadURL', guestURL.toString()); } } }); } public getSafe = () => { const that = this; return { get href () { return that.href; }, set href (newValue) { that.href = newValue; }, get hash () { return that.hash; }, set hash (newValue) { that.hash = newValue; }, get host () { return that.host; }, set host (newValue) { that.host = newValue; }, get hostname () { return that.hostname; }, set hostname (newValue) { that.hostname = newValue; }, get origin () { return that.origin; }, set origin (newValue) { that.origin = newValue; }, get pathname () { return that.pathname; }, set pathname (newValue) { that.pathname = newValue; }, get port () { return that.port; }, set port (newValue) { that.port = newValue; }, get protocol () { return that.protocol; }, set protocol (newValue) { that.protocol = newValue; }, get search () { return that.search; }, set search (newValue) { that.search = newValue; } }; } constructor (guestId: number) { // eslint will consider the constructor "useless" // unless we assign them in the body. It's fine, that's what // TS would do anyway. this.guestId = guestId; this.getGuestURL = this.getGuestURL.bind(this); } public toString (): string { return this.href; } private getGuestURL (): URL | null { const maybeURL = this._invokeWebContentsMethodSync('getURL') as string; // When there's no previous frame the url will be blank, so account for that here // to prevent url parsing errors on an empty string. const urlString = maybeURL !== '' ? maybeURL : 'about:blank'; try { return new URL(urlString); } catch (e) { console.error('LocationProxy: failed to parse string', urlString, e); } return null; } private _invokeWebContentsMethod (method: string, ...args: any[]) { return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD, this.guestId, method, ...args); } private _invokeWebContentsMethodSync (method: string, ...args: any[]) { return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD, this.guestId, method, ...args); } } interface SafelyBoundBrowserWindowProxy { location: WindowProxy['location']; blur: WindowProxy['blur']; close: WindowProxy['close']; eval: typeof eval; // eslint-disable-line no-eval focus: WindowProxy['focus']; print: WindowProxy['print']; postMessage: WindowProxy['postMessage']; closed: boolean; } class BrowserWindowProxy { public closed: boolean = false private _location: LocationProxy private guestId: number // TypeScript doesn't allow getters/accessors with different types, // so for now, we'll have to make do with an "any" in the mix. // https://github.com/Microsoft/TypeScript/issues/2521 public get location (): LocationProxy | any { return this._location.getSafe(); } public set location (url: string | any) { url = resolveURL(url, this.location.href); this._invokeWebContentsMethod('loadURL', url); } constructor (guestId: number) { this.guestId = guestId; this._location = new LocationProxy(guestId); ipcRendererInternal.once(`${IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_CLOSED}_${guestId}`, () => { removeProxy(guestId); this.closed = true; }); } public getSafe = (): SafelyBoundBrowserWindowProxy => { const that = this; return { postMessage: this.postMessage, blur: this.blur, close: this.close, focus: this.focus, print: this.print, eval: this.eval, get location () { return that.location; }, set location (url: string | any) { that.location = url; }, get closed () { return that.closed; } }; } public close = () => { this._invokeWindowMethod('destroy'); } public focus = () => { this._invokeWindowMethod('focus'); } public blur = () => { this._invokeWindowMethod('blur'); } public print = () => { this._invokeWebContentsMethod('print'); } public postMessage = (message: any, targetOrigin: string) => { ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE, this.guestId, message, toString(targetOrigin), window.location.origin); } public eval = (code: string) => { this._invokeWebContentsMethod('executeJavaScript', code); } private _invokeWindowMethod (method: string, ...args: any[]) { return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_METHOD, this.guestId, method, ...args); } private _invokeWebContentsMethod (method: string, ...args: any[]) { return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD, this.guestId, method, ...args); } } export const windowSetup = ( guestInstanceId: number, openerId: number, isHiddenPage: boolean, usesNativeWindowOpen: boolean) => { if (!process.sandboxed && !guestInstanceId) { // Override default window.close. window.close = function () { ipcRendererInternal.send(IPC_MESSAGES.BROWSER_WINDOW_CLOSE); }; if (contextIsolationEnabled) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['close'], window.close); } if (!usesNativeWindowOpen) { // TODO(MarshallOfSound): Make compatible with ctx isolation without hole-punch // Make the browser window or guest view emit "new-window" event. window.open = function (url?: string, frameName?: string, features?: string) { if (url != null && url !== '') { url = resolveURL(url, location.href); } const guestId = ipcRendererInternal.sendSync(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_OPEN, url, toString(frameName), toString(features)); if (guestId != null) { return getOrCreateProxy(guestId) as any as WindowProxy; } else { return null; } }; if (contextIsolationEnabled) internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['open'], window.open); } // If this window uses nativeWindowOpen, but its opener window does not, we // need to proxy window.opener in order to let the page communicate with its // opener. // Additionally, windows opened from a nativeWindowOpen child of a // non-nativeWindowOpen parent will initially have their WebPreferences // copied from their opener before having them updated, meaning openerId is // initially incorrect. We detect this situation by checking for // window.opener, which will be non-null for a natively-opened child, so we // can ignore the openerId in that case, since it's incorrectly copied from // the parent. This is, uh, confusing, so here's a diagram that will maybe // help? // // [ grandparent window ] --> [ parent window ] --> [ child window ] // n.W.O = false n.W.O = true n.W.O = true // id = 1 id = 2 id = 3 // openerId = 0 openerId = 1 openerId = 1 <- !!wrong!! // opener = null opener = null opener = [parent window] if (openerId && !window.opener) { window.opener = getOrCreateProxy(openerId); if (contextIsolationEnabled) internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['opener'], window.opener); } // But we do not support prompt(). window.prompt = function () { throw new Error('prompt() is and will not be supported.'); }; if (contextIsolationEnabled) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['prompt'], window.prompt); if (!usesNativeWindowOpen || openerId) { ipcRendererInternal.on(IPC_MESSAGES.GUEST_WINDOW_POSTMESSAGE, function ( _event, sourceId: number, message: any, sourceOrigin: string ) { // Manually dispatch event instead of using postMessage because we also need to // set event.source. // // Why any? We can't construct a MessageEvent and we can't // use `as MessageEvent` because you're not supposed to override // data, origin, and source const event: any = document.createEvent('Event'); event.initEvent('message', false, false); event.data = message; event.origin = sourceOrigin; event.source = getOrCreateProxy(sourceId); window.dispatchEvent(event as MessageEvent); }); } if (guestInstanceId) { // Webview `document.visibilityState` tracks window visibility (and ignores // the actual element visibility) for backwards compatibility. // See discussion in #9178. // // Note that this results in duplicate visibilitychange events (since // Chromium also fires them) and potentially incorrect visibility change. // We should reconsider this decision for Electron 2.0. let cachedVisibilityState = isHiddenPage ? 'hidden' : 'visible'; // Subscribe to visibilityState changes. ipcRendererInternal.on(IPC_MESSAGES.GUEST_INSTANCE_VISIBILITY_CHANGE, function (_event, visibilityState: VisibilityState) { if (cachedVisibilityState !== visibilityState) { cachedVisibilityState = visibilityState; document.dispatchEvent(new Event('visibilitychange')); } }); // Make document.hidden and document.visibilityState return the correct value. const getDocumentHidden = () => cachedVisibilityState !== 'visible'; Object.defineProperty(document, 'hidden', { get: getDocumentHidden }); if (contextIsolationEnabled) internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['document', 'hidden'], getDocumentHidden); const getDocumentVisibilityState = () => cachedVisibilityState; Object.defineProperty(document, 'visibilityState', { get: getDocumentVisibilityState }); if (contextIsolationEnabled) internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['document', 'visibilityState'], getDocumentVisibilityState); } };