From 6cd75744efec8772dcaa88593a2c0c1ea4fcc5a6 Mon Sep 17 00:00:00 2001 From: Felix Rieseberg Date: Thu, 14 Feb 2019 05:52:38 -0800 Subject: [PATCH] refactor: Port window-setup to TS (#16894) * refactor: Port window-setup to TS * refactor: Make the linter happy * refactor: Sneaky little TS error * refactor: Correctly import window-setup * refactor: Implement feedback <3 * refactor: Allow decorators in TS * refactor: Use named windowSetup in isolatedRenderer * refactor: Help TS understand * refactor: Welp, use createEvent again * refactor: Use the correct target in the decorator --- filenames.gni | 2 +- lib/isolated_renderer/init.js | 3 +- lib/renderer/init.js | 3 +- lib/renderer/window-setup.js | 239 ----------------------------- lib/renderer/window-setup.ts | 276 ++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 6 files changed, 282 insertions(+), 243 deletions(-) delete mode 100644 lib/renderer/window-setup.js create mode 100644 lib/renderer/window-setup.ts diff --git a/filenames.gni b/filenames.gni index 188473cc8957..5cbcbe976894 100644 --- a/filenames.gni +++ b/filenames.gni @@ -72,7 +72,7 @@ filenames = { "lib/renderer/remote.js", "lib/renderer/security-warnings.js", "lib/renderer/web-frame-init.js", - "lib/renderer/window-setup.js", + "lib/renderer/window-setup.ts", "lib/renderer/web-view/guest-view-internal.js", "lib/renderer/web-view/web-view-attributes.js", "lib/renderer/web-view/web-view-constants.js", diff --git a/lib/isolated_renderer/init.js b/lib/isolated_renderer/init.js index dc6223279aa1..058c89720fcb 100644 --- a/lib/isolated_renderer/init.js +++ b/lib/isolated_renderer/init.js @@ -18,5 +18,6 @@ const isolatedWorldArgs = v8Util.getHiddenValue(isolatedWorld, 'isolated-world-a if (isolatedWorldArgs) { const { ipcRenderer, guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen } = isolatedWorldArgs - require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) + const { windowSetup } = require('@electron/internal/renderer/window-setup') + windowSetup(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) } diff --git a/lib/renderer/init.js b/lib/renderer/init.js index cd2ffc57389b..f6916227cf3c 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -73,7 +73,8 @@ switch (window.location.protocol) { break default: { // Override default web functions. - require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) + const { windowSetup } = require('@electron/internal/renderer/window-setup') + windowSetup(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) // Inject content scripts. if (process.isMainFrame) { diff --git a/lib/renderer/window-setup.js b/lib/renderer/window-setup.js deleted file mode 100644 index 76817c67a7c9..000000000000 --- a/lib/renderer/window-setup.js +++ /dev/null @@ -1,239 +0,0 @@ -'use strict' - -// This file should have no requires since it is used by the isolated context -// preload bundle. Instead arguments should be passed in for everything it -// needs. - -// This file implements the following APIs: -// - window.history.back() -// - window.history.forward() -// - window.history.go() -// - window.history.length -// - window.open() -// - window.opener.blur() -// - window.opener.close() -// - window.opener.eval() -// - window.opener.focus() -// - window.opener.location -// - window.opener.print() -// - window.opener.postMessage() -// - window.prompt() -// - document.hidden -// - document.visibilityState - -const { defineProperty, defineProperties } = Object - -// Helper function to resolve relative url. -const a = window.document.createElement('a') -const resolveURL = function (url) { - a.href = url - return a.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) => { - return value != null ? `${value}` : value -} - -const windowProxies = {} - -const getOrCreateProxy = (ipcRenderer, guestId) => { - let proxy = windowProxies[guestId] - if (proxy == null) { - proxy = new BrowserWindowProxy(ipcRenderer, guestId) - windowProxies[guestId] = proxy - } - return proxy -} - -const removeProxy = (guestId) => { - delete windowProxies[guestId] -} - -function LocationProxy (ipcRenderer, guestId) { - const getGuestURL = function () { - const urlString = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'getURL') - try { - return new URL(urlString) - } catch (e) { - console.error('LocationProxy: failed to parse string', urlString, e) - } - - return null - } - - const propertyProxyFor = function (property) { - return { - get: function () { - const guestURL = getGuestURL() - const value = guestURL ? guestURL[property] : '' - return value === undefined ? '' : value - }, - set: function (newVal) { - const guestURL = getGuestURL() - if (guestURL) { - guestURL[property] = newVal - return ipcRenderer.sendSync( - 'ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', - guestId, 'loadURL', guestURL.toString()) - } - } - } - } - - defineProperties(this, { - hash: propertyProxyFor('hash'), - href: propertyProxyFor('href'), - host: propertyProxyFor('host'), - hostname: propertyProxyFor('hostname'), - origin: propertyProxyFor('origin'), - pathname: propertyProxyFor('pathname'), - port: propertyProxyFor('port'), - protocol: propertyProxyFor('protocol'), - search: propertyProxyFor('search') - }) - - this.toString = function () { - return this.href - } -} - -function BrowserWindowProxy (ipcRenderer, guestId) { - this.closed = false - - const location = new LocationProxy(ipcRenderer, guestId) - defineProperty(this, 'location', { - get: function () { - return location - }, - set: function (url) { - url = resolveURL(url) - return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'loadURL', url) - } - }) - - ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => { - removeProxy(guestId) - this.closed = true - }) - - this.close = () => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', guestId) - } - - this.focus = () => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'focus') - } - - this.blur = () => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'blur') - } - - this.print = () => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'print') - } - - this.postMessage = (message, targetOrigin) => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, toString(targetOrigin), window.location.origin) - } - - this.eval = (...args) => { - ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'executeJavaScript', ...args) - } -} - -module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) => { - if (guestInstanceId == null) { - // Override default window.close. - window.close = function () { - ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE') - } - } - - if (!usesNativeWindowOpen) { - // Make the browser window or guest view emit "new-window" event. - window.open = function (url, frameName, features) { - if (url != null && url !== '') { - url = resolveURL(url) - } - const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features)) - if (guestId != null) { - return getOrCreateProxy(ipcRenderer, guestId) - } else { - return null - } - } - - if (openerId != null) { - window.opener = getOrCreateProxy(ipcRenderer, openerId) - } - } - - // But we do not support prompt(). - window.prompt = function () { - throw new Error('prompt() is and will not be supported.') - } - - ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) { - // Manually dispatch event instead of using postMessage because we also need to - // set event.source. - event = document.createEvent('Event') - event.initEvent('message', false, false) - event.data = message - event.origin = sourceOrigin - event.source = getOrCreateProxy(ipcRenderer, sourceId) - window.dispatchEvent(event) - }) - - window.history.back = function () { - ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK') - } - - window.history.forward = function () { - ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD') - } - - window.history.go = function (offset) { - ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset) - } - - defineProperty(window.history, 'length', { - get: function () { - return ipcRenderer.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH') - } - }) - - if (guestInstanceId != null) { - // 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 = hiddenPage ? 'hidden' : 'visible' - - // Subscribe to visibilityState changes. - ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (event, visibilityState) { - if (cachedVisibilityState !== visibilityState) { - cachedVisibilityState = visibilityState - document.dispatchEvent(new Event('visibilitychange')) - } - }) - - // Make document.hidden and document.visibilityState return the correct value. - defineProperty(document, 'hidden', { - get: function () { - return cachedVisibilityState !== 'visible' - } - }) - - defineProperty(document, 'visibilityState', { - get: function () { - return cachedVisibilityState - } - }) - } -} diff --git a/lib/renderer/window-setup.ts b/lib/renderer/window-setup.ts new file mode 100644 index 000000000000..17c653e1f2a0 --- /dev/null +++ b/lib/renderer/window-setup.ts @@ -0,0 +1,276 @@ +// This file should have no requires since it is used by the isolated context +// preload bundle. Instead arguments should be passed in for everything it +// needs. + +// This file implements the following APIs: +// - window.history.back() +// - window.history.forward() +// - window.history.go() +// - window.history.length +// - window.open() +// - window.opener.blur() +// - window.opener.close() +// - window.opener.eval() +// - window.opener.focus() +// - window.opener.location +// - window.opener.print() +// - window.opener.postMessage() +// - window.prompt() +// - document.hidden +// - document.visibilityState + +const { defineProperty, defineProperties } = Object + +// Helper function to resolve relative url. +const a = window.document.createElement('a') +const resolveURL = function (url: string) { + a.href = url + return a.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: Record = {} + +const getOrCreateProxy = (ipcRenderer: Electron.IpcRenderer, guestId: number) => { + let proxy = windowProxies[guestId] + if (proxy == null) { + proxy = new BrowserWindowProxy(ipcRenderer, guestId) + windowProxies[guestId] = proxy + } + return proxy +} + +const removeProxy = (guestId: number) => { + delete windowProxies[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 ipcRenderer: Electron.IpcRenderer; + 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, { + get: function (): T | string { + const guestURL = this.getGuestURL() + const value = guestURL ? guestURL[propertyKey] : '' + return value === undefined ? '' : value + }, + set: function (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 anway. + (guestURL as any)[propertyKey] = newVal + + return this.ipcRenderer.sendSync( + 'ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', + this.guestId, 'loadURL', guestURL.toString()) + } + } + }) + } + + constructor (ipcRenderer: Electron.IpcRenderer, 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.ipcRenderer = ipcRenderer + this.guestId = guestId + this.getGuestURL = this.getGuestURL.bind(this) + } + + public toString (): string { + return this.href + } + + private getGuestURL (): URL | null { + const urlString = this.ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'getURL') + try { + return new URL(urlString) + } catch (e) { + console.error('LocationProxy: failed to parse string', urlString, e) + } + + return null + } +} + +class BrowserWindowProxy { + public closed: boolean = false + + private _location: LocationProxy + private guestId: number + private ipcRenderer: Electron.IpcRenderer + + // 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 + } + public set location (url: string | any) { + url = resolveURL(url) + this.ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'loadURL', url) + } + + constructor (ipcRenderer: Electron.IpcRenderer, guestId: number) { + this.guestId = guestId + this.ipcRenderer = ipcRenderer + this._location = new LocationProxy(ipcRenderer, guestId) + + ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => { + removeProxy(guestId) + this.closed = true + }) + } + + public close () { + this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', this.guestId) + } + + public focus () { + this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'focus') + } + + public blur () { + this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'blur') + } + + public print () { + this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'print') + } + + public postMessage (message: any, targetOrigin: any) { + this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, toString(targetOrigin), window.location.origin) + } + + public eval (...args: any[]) { + this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'executeJavaScript', ...args) + } +} + +export const windowSetup = ( + ipcRenderer: Electron.IpcRenderer, guestInstanceId: number, openerId: number, isHiddenPage: boolean, usesNativeWindowOpen: boolean +) => { + if (guestInstanceId == null) { + // Override default window.close. + window.close = function () { + ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE') + } + } + + if (!usesNativeWindowOpen) { + // Make the browser window or guest view emit "new-window" event. + (window as any).open = function (url?: string, frameName?: string, features?: string) { + if (url != null && url !== '') { + url = resolveURL(url) + } + const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features)) + if (guestId != null) { + return getOrCreateProxy(ipcRenderer, guestId) + } else { + return null + } + } + + if (openerId != null) { + window.opener = getOrCreateProxy(ipcRenderer, openerId) + } + } + + // But we do not support prompt(). + window.prompt = function () { + throw new Error('prompt() is and will not be supported.') + } + + ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function ( + _event: Electron.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(ipcRenderer, sourceId) + + window.dispatchEvent(event as MessageEvent) + }) + + window.history.back = function () { + ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK') + } + + window.history.forward = function () { + ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD') + } + + window.history.go = function (offset: number) { + ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset) + } + + defineProperty(window.history, 'length', { + get: function () { + return ipcRenderer.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH') + } + }) + + if (guestInstanceId != null) { + // 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. + ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (_event: Electron.Event, visibilityState: VisibilityState) { + if (cachedVisibilityState !== visibilityState) { + cachedVisibilityState = visibilityState + document.dispatchEvent(new Event('visibilitychange')) + } + }) + + // Make document.hidden and document.visibilityState return the correct value. + defineProperty(document, 'hidden', { + get: function () { + return cachedVisibilityState !== 'visible' + } + }) + + defineProperty(document, 'visibilityState', { + get: function () { + return cachedVisibilityState + } + }) + } +} diff --git a/tsconfig.json b/tsconfig.json index 47362c54f92b..e2fbd028f2d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "dom.iterable" ], "sourceMap": true, - "experimentalDecorators": false, + "experimentalDecorators": true, "strict": true, "baseUrl": ".", "allowJs": true,