Skip to content

Commit

Permalink
refactor: implement <webview> using contextBridge (electron#29037)
Browse files Browse the repository at this point in the history
* refactor: implement <webview> using contextBridge

* chore: address PR feedback

* chore: address PR feedback

* fix: check for HTMLIFrameElement instance in attachGuest
  • Loading branch information
miniak authored May 15, 2021
1 parent 5e6f834 commit c68c65f
Show file tree
Hide file tree
Showing 17 changed files with 220 additions and 214 deletions.
4 changes: 4 additions & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,13 @@ auto_filenames = {
]

isolated_bundle_deps = [
"lib/common/type-utils.ts",
"lib/common/web-view-methods.ts",
"lib/isolated_renderer/init.ts",
"lib/renderer/web-view/web-view-attributes.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
Expand Down
7 changes: 1 addition & 6 deletions lib/browser/api/web-contents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, ipcMain, session, deprecate, webFrameMain } from 'electron/main';
import { app, ipcMain, session, webFrameMain } from 'electron/main';
import type { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron/main';

import * as url from 'url';
Expand Down Expand Up @@ -709,11 +709,6 @@ WebContents.prototype._init = function () {
}
});
});

const prefs = this.getWebPreferences() || {};
if (prefs.webviewTag && prefs.contextIsolation) {
deprecate.log('Security Warning: A WebContents was just created with both webviewTag and contextIsolation enabled. This combination is fundamentally less secure and effectively bypasses the protections of contextIsolation. We strongly recommend you move away from webviews to OOPIF or BrowserView in order for your app to be more secure');
}
}

this.on('login', (event, ...args) => {
Expand Down
16 changes: 9 additions & 7 deletions lib/common/type-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { nativeImage } = process._linkedBinding('electron_common_native_image');
function getCreateNativeImage () {
return process._linkedBinding('electron_common_native_image').nativeImage.createEmpty;
}

export function isPromise (val: any) {
return (
Expand Down Expand Up @@ -57,8 +59,8 @@ function serializeNativeImage (image: Electron.NativeImage) {
return { __ELECTRON_SERIALIZED_NativeImage__: true, representations };
}

function deserializeNativeImage (value: any) {
const image = nativeImage.createEmpty();
function deserializeNativeImage (value: any, createNativeImage: typeof Electron.nativeImage['createEmpty']) {
const image = createNativeImage();

// Use Buffer when there's only one representation for better perf.
// This avoids compressing to/from PNG where it's not necessary to
Expand Down Expand Up @@ -93,15 +95,15 @@ export function serialize (value: any): any {
}
}

export function deserialize (value: any): any {
export function deserialize (value: any, createNativeImage: typeof Electron.nativeImage['createEmpty'] = getCreateNativeImage()): any {
if (value && value.__ELECTRON_SERIALIZED_NativeImage__) {
return deserializeNativeImage(value);
return deserializeNativeImage(value, createNativeImage);
} else if (Array.isArray(value)) {
return value.map(deserialize);
return value.map(value => deserialize(value, createNativeImage));
} else if (isSerializableObject(value)) {
return value;
} else if (value instanceof Object) {
return objectMap(value, deserialize);
return objectMap(value, value => deserialize(value, createNativeImage));
} else {
return value;
}
Expand Down
12 changes: 3 additions & 9 deletions lib/isolated_renderer/init.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
/* global nodeProcess, isolatedWorld */
/* global isolatedApi */

import type * as webViewElementModule from '@electron/internal/renderer/web-view/web-view-element';

process._linkedBinding = nodeProcess._linkedBinding;

const v8Util = process._linkedBinding('electron_common_v8_util');

const webViewImpl = v8Util.getHiddenValue(isolatedWorld, 'web-view-impl');

if (webViewImpl) {
if (isolatedApi.guestViewInternal) {
// Must setup the WebView element in main world.
const { setupWebView } = require('@electron/internal/renderer/web-view/web-view-element') as typeof webViewElementModule;
setupWebView(webViewImpl as any);
setupWebView(isolatedApi);
}
68 changes: 43 additions & 25 deletions lib/renderer/web-view/guest-view-internal.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
import { webFrame } from 'electron';
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils';
import { webViewEvents } from '@electron/internal/common/web-view-events';

import { WebViewImpl } from '@electron/internal/renderer/web-view/web-view-impl';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';

const { mainFrame: webFrame } = process._linkedBinding('electron_renderer_web_frame');

export interface GuestViewDelegate {
dispatchEvent (eventName: string, props: Record<string, any>): void;
reset(): void;
}

const DEPRECATED_EVENTS: Record<string, string> = {
'page-title-updated': 'page-title-set'
} as const;

const dispatchEvent = function (
webView: WebViewImpl, eventName: string, eventKey: string, ...args: Array<any>
) {
const dispatchEvent = function (delegate: GuestViewDelegate, eventName: string, eventKey: string, ...args: Array<any>) {
if (DEPRECATED_EVENTS[eventName] != null) {
dispatchEvent(webView, DEPRECATED_EVENTS[eventName], eventKey, ...args);
dispatchEvent(delegate, DEPRECATED_EVENTS[eventName], eventKey, ...args);
}

const props: Record<string, any> = {};
webViewEvents[eventKey].forEach((prop, index) => {
props[prop] = args[index];
});

webView.dispatchEvent(eventName, props);

if (eventName === 'load-commit') {
webView.onLoadCommit(props);
} else if (eventName === '-focus-change') {
webView.onFocusChange();
}
delegate.dispatchEvent(eventName, props);
};

export function registerEvents (webView: WebViewImpl, viewInstanceId: number) {
export function registerEvents (viewInstanceId: number, delegate: GuestViewDelegate) {
ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DESTROY_GUEST}-${viewInstanceId}`, function () {
webView.guestInstanceId = undefined;
webView.reset();
webView.dispatchEvent('destroyed');
delegate.reset();
delegate.dispatchEvent('destroyed', {});
});

ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT}-${viewInstanceId}`, function (event, eventName, ...args) {
dispatchEvent(webView, eventName, eventName, ...args);
dispatchEvent(delegate, eventName, eventName, ...args);
});

ipcRendererInternal.on(`${IPC_MESSAGES.GUEST_VIEW_INTERNAL_IPC_MESSAGE}-${viewInstanceId}`, function (event, channel, ...args) {
webView.dispatchEvent('ipc-message', { channel, args });
delegate.dispatchEvent('ipc-message', { channel, args });
});
}

Expand All @@ -57,16 +52,39 @@ export function createGuest (params: Record<string, any>): Promise<number> {
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_GUEST, params);
}

export function attachGuest (
elementInstanceId: number, guestInstanceId: number, params: Record<string, any>, contentWindow: Window
) {
const embedderFrameId = webFrame.getWebFrameId(contentWindow);
export function attachGuest (iframe: HTMLIFrameElement, elementInstanceId: number, guestInstanceId: number, params: Record<string, any>) {
if (!(iframe instanceof HTMLIFrameElement)) {
throw new Error('Invalid embedder frame');
}

const embedderFrameId = webFrame.getWebFrameId(iframe.contentWindow!);
if (embedderFrameId < 0) { // this error should not happen.
throw new Error('Invalid embedder frame');
}
ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_ATTACH_GUEST, embedderFrameId, elementInstanceId, guestInstanceId, params);

return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_ATTACH_GUEST, embedderFrameId, elementInstanceId, guestInstanceId, params);
}

export function detachGuest (guestInstanceId: number) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_DETACH_GUEST, guestInstanceId);
}

export function capturePage (guestInstanceId: number, args: any[]) {
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CAPTURE_PAGE, guestInstanceId, args);
}

export function invoke (guestInstanceId: number, method: string, args: any[]) {
return ipcRendererInternal.invoke(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, guestInstanceId, method, args);
}

export function invokeSync (guestInstanceId: number, method: string, args: any[]) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, guestInstanceId, method, args);
}

export function propertyGet (guestInstanceId: number, name: string) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET, guestInstanceId, name);
}

export function propertySet (guestInstanceId: number, name: string, value: any) {
return ipcRendererUtils.invokeSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET, guestInstanceId, name, value);
}
36 changes: 16 additions & 20 deletions lib/renderer/web-view/web-view-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
// modules must be passed from outside, all included files must be plain JS.

import { WEB_VIEW_CONSTANTS } from '@electron/internal/renderer/web-view/web-view-constants';
import type * as webViewImplModule from '@electron/internal/renderer/web-view/web-view-impl';
import { WebViewImpl, WebViewImplHooks, setupMethods } from '@electron/internal/renderer/web-view/web-view-impl';
import type { SrcAttribute } from '@electron/internal/renderer/web-view/web-view-attributes';

const internals = new WeakMap<HTMLElement, webViewImplModule.WebViewImpl>();
const internals = new WeakMap<HTMLElement, WebViewImpl>();

// Return a WebViewElement class that is defined in this context.
const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
const { guestViewInternal, WebViewImpl } = webViewImpl;
const defineWebViewElement = (hooks: WebViewImplHooks) => {
return class WebViewElement extends HTMLElement {
static get observedAttributes () {
return [
Expand All @@ -38,13 +37,7 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {

constructor () {
super();
const internal = new WebViewImpl(this);
internal.dispatchEventInMainWorld = (eventName, props) => {
const event = new Event(eventName);
Object.assign(event, props);
return internal.webviewNode.dispatchEvent(event);
};
internals.set(this, internal);
internals.set(this, new WebViewImpl(this, hooks));
}

getWebContentsId () {
Expand All @@ -61,7 +54,10 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
return;
}
if (!internal.elementAttached) {
guestViewInternal.registerEvents(internal, internal.viewInstanceId);
hooks.guestViewInternal.registerEvents(internal.viewInstanceId, {
dispatchEvent: internal.dispatchEvent.bind(internal),
reset: internal.reset.bind(internal)
});
internal.elementAttached = true;
(internal.attributes.get(WEB_VIEW_CONSTANTS.ATTRIBUTE_SRC) as SrcAttribute).parse();
}
Expand All @@ -79,9 +75,9 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
if (!internal) {
return;
}
guestViewInternal.deregisterEvents(internal.viewInstanceId);
hooks.guestViewInternal.deregisterEvents(internal.viewInstanceId);
if (internal.guestInstanceId) {
guestViewInternal.detachGuest(internal.guestInstanceId);
hooks.guestViewInternal.detachGuest(internal.guestInstanceId);
}
internal.elementAttached = false;
internal.reset();
Expand All @@ -90,15 +86,15 @@ const defineWebViewElement = (webViewImpl: typeof webViewImplModule) => {
};

// Register <webview> custom element.
const registerWebViewElement = (webViewImpl: typeof webViewImplModule) => {
const registerWebViewElement = (hooks: WebViewImplHooks) => {
// I wish eslint wasn't so stupid, but it is
// eslint-disable-next-line
const WebViewElement = defineWebViewElement(webViewImpl) as unknown as typeof ElectronInternal.WebViewElement
const WebViewElement = defineWebViewElement(hooks) as unknown as typeof ElectronInternal.WebViewElement

webViewImpl.setupMethods(WebViewElement);
setupMethods(WebViewElement, hooks);

// The customElements.define has to be called in a special scope.
webViewImpl.webFrame.allowGuestViewElementDefinition(window, () => {
hooks.allowGuestViewElementDefinition(window, () => {
window.customElements.define('webview', WebViewElement);
window.WebView = WebViewElement;

Expand All @@ -116,14 +112,14 @@ const registerWebViewElement = (webViewImpl: typeof webViewImplModule) => {
};

// Prepare to register the <webview> element.
export const setupWebView = (webViewImpl: typeof webViewImplModule) => {
export const setupWebView = (hooks: WebViewImplHooks) => {
const useCapture = true;
const listener = (event: Event) => {
if (document.readyState === 'loading') {
return;
}

registerWebViewElement(webViewImpl);
registerWebViewElement(hooks);

window.removeEventListener(event.type, listener, useCapture);
};
Expand Down
Loading

0 comments on commit c68c65f

Please sign in to comment.