feat: add webContents.setWindowOpenHandler API (#24517)

Co-authored-by: Jeremy Rose <jeremya@chromium.org>
This commit is contained in:
loc 2020-11-10 09:06:03 -08:00 committed by GitHub
parent 6b222a2d8a
commit 0b85fdf26c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2087 additions and 885 deletions

View file

@ -1,13 +1,12 @@
import { app, ipcMain, session, deprecate } from 'electron/main';
import type { MenuItem, MenuItemConstructorOptions } from 'electron/main';
import { app, ipcMain, session, deprecate, BrowserWindowConstructorOptions } from 'electron/main';
import type { MenuItem, MenuItemConstructorOptions, LoadURLOptions } from 'electron/main';
import * as url from 'url';
import * as path from 'path';
import { internalWindowOpen } from '@electron/internal/browser/guest-window-manager';
import { openGuestWindow, makeWebPreferences } from '@electron/internal/browser/guest-window-manager';
import { NavigationController } from '@electron/internal/browser/navigation-controller';
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { parseFeatures } from '@electron/internal/common/parse-features-string';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
@ -21,6 +20,8 @@ const getNextId = function () {
return ++nextId;
};
type PostData = LoadURLOptions['postData']
/* eslint-disable camelcase */
type MediaSize = {
name: string,
@ -439,6 +440,40 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
}));
};
WebContents.prototype.setWindowOpenHandler = function (handler: (details: Electron.HandlerDetails) => ({action: 'allow'} | {action: 'deny', overrideBrowserWindowOptions?: BrowserWindowConstructorOptions})) {
this._windowOpenHandler = handler;
};
WebContents.prototype._callWindowOpenHandler = function (event: any, url: string, frameName: string, rawFeatures: string): BrowserWindowConstructorOptions | null {
if (!this._windowOpenHandler) {
return null;
}
const response = this._windowOpenHandler({ url, frameName, features: rawFeatures });
if (typeof response !== 'object') {
event.preventDefault();
console.error(`The window open handler response must be an object, but was instead of type '${typeof response}'.`);
return null;
}
if (response === null) {
event.preventDefault();
console.error('The window open handler response must be an object, but was instead null.');
return null;
}
if (response.action === 'deny') {
event.preventDefault();
return null;
} else if (response.action === 'allow') {
if (typeof response.overrideBrowserWindowOptions === 'object' && response.overrideBrowserWindowOptions !== null) { return response.overrideBrowserWindowOptions; } else { return {}; }
} else {
event.preventDefault();
console.error('The window open handler response must be an object with an \'action\' property of \'allow\' or \'deny\'.');
return null;
}
};
const addReplyToEvent = (event: any) => {
event.reply = (...args: any[]) => {
event.sender.sendToFrame(event.frameId, ...args);
@ -490,6 +525,8 @@ WebContents.prototype._init = function () {
this.getActiveIndex = navigationController.getActiveIndex.bind(navigationController);
this.length = navigationController.length.bind(navigationController);
this._windowOpenHandler = null;
// Every remote callback from renderer process would add a listener to the
// render-view-deleted event, so ignore the listeners warning.
this.setMaxListeners(0);
@ -570,43 +607,67 @@ WebContents.prototype._init = function () {
if (this.getType() !== 'remote') {
// Make new windows requested by links behave like "window.open".
this.on('-new-window' as any, (event: any, url: string, frameName: string, disposition: string,
rawFeatures: string, referrer: string, postData: Electron.UploadRawData[]) => {
const { options, webPreferences, additionalFeatures } = parseFeatures(rawFeatures);
const mergedOptions = {
show: true,
width: 800,
height: 600,
title: frameName,
webPreferences,
...options
};
rawFeatures: string, referrer: any, postData: PostData) => {
openGuestWindow({
event,
embedder: event.sender,
disposition,
referrer,
postData,
overrideBrowserWindowOptions: {},
windowOpenArgs: {
url,
frameName,
features: rawFeatures
}
});
});
internalWindowOpen(event, url, referrer, frameName, disposition, mergedOptions, additionalFeatures, postData);
let windowOpenOverriddenOptions: BrowserWindowConstructorOptions | null = null;
this.on('-will-add-new-contents' as any, (event: any, url: string, frameName: string, rawFeatures: string) => {
windowOpenOverriddenOptions = this._callWindowOpenHandler(event, url, frameName, rawFeatures);
if (!event.defaultPrevented) {
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
// Allow setting of backgroundColor as a webPreference even though
// it's technically a BrowserWindowConstructorOptions option because
// we need to access it in the renderer at init time.
backgroundColor: windowOpenOverriddenOptions.backgroundColor,
...windowOpenOverriddenOptions.webPreferences
} : undefined;
this._setNextChildWebPreferences(
makeWebPreferences({ embedder: event.sender, secureOverrideWebPreferences })
);
}
});
// Create a new browser window for the native implementation of
// "window.open", used in sandbox and nativeWindowOpen mode.
this.on('-add-new-contents' as any, (event: any, webContents: Electron.WebContents, disposition: string,
userGesture: boolean, left: number, top: number, width: number, height: number, url: string, frameName: string,
referrer: string, rawFeatures: string, postData: Electron.UploadRawData[]) => {
_userGesture: boolean, _left: number, _top: number, _width: number, _height: number, url: string, frameName: string,
referrer: Electron.Referrer, rawFeatures: string, postData: PostData) => {
const overriddenOptions = windowOpenOverriddenOptions || undefined;
windowOpenOverriddenOptions = null;
if ((disposition !== 'foreground-tab' && disposition !== 'new-window' &&
disposition !== 'background-tab')) {
event.preventDefault();
return;
}
const { options, webPreferences, additionalFeatures } = parseFeatures(rawFeatures);
const mergedOptions = {
show: true,
width: 800,
height: 600,
webContents,
title: frameName,
webPreferences,
...options
};
internalWindowOpen(event, url, referrer, frameName, disposition, mergedOptions, additionalFeatures, postData);
openGuestWindow({
event,
embedder: event.sender,
guest: webContents,
overrideBrowserWindowOptions: overriddenOptions,
disposition,
referrer,
postData,
windowOpenArgs: {
url,
frameName,
features: rawFeatures
}
});
});
const prefs = this.getWebPreferences() || {};

View file

@ -1,360 +1,300 @@
import * as electron from 'electron/main';
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
/**
* Create and minimally track guest windows at the direction of the renderer
* (via window.open). Here, "guest" roughly means "child" it's not necessarily
* emblematic of its process status; both in-process (same-origin
* nativeWindowOpen) and out-of-process (cross-origin nativeWindowOpen and
* BrowserWindowProxy) are created here. "Embedder" roughly means "parent."
*/
import { BrowserWindow } from 'electron/main';
import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';
import { parseFeatures } from '@electron/internal/common/parse-features-string';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
const { isSameOrigin } = process._linkedBinding('electron_common_v8_util');
const { BrowserWindow } = electron;
const hasProp = {}.hasOwnProperty;
const frameToGuest = new Map<string, Electron.BrowserWindow>();
// Security options that child windows will always inherit from parent windows
const inheritedWebPreferences = new Map([
['contextIsolation', true],
['javascript', false],
['nativeWindowOpen', true],
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true],
['webviewTag', false],
['nodeIntegrationInSubFrames', false],
['enableWebSQL', false]
]);
// Copy attribute of |parent| to |child| if it is not defined in |child|.
const mergeOptions = function (child: Record<string, any>, parent: Record<string, any>, visited?: Set<Record<string, any>>) {
// Check for circular reference.
if (visited == null) visited = new Set();
if (visited.has(parent)) return;
visited.add(parent);
for (const key in parent) {
if (key === 'type') continue;
if (!hasProp.call(parent, key)) continue;
if (key in child && key !== 'webPreferences') continue;
const value = parent[key];
if (typeof value === 'object' && !Array.isArray(value)) {
child[key] = mergeOptions(child[key] || {}, value, visited);
} else {
child[key] = value;
}
}
visited.delete(parent);
return child;
};
// Merge |options| with the |embedder|'s window's options.
const mergeBrowserWindowOptions = function (embedder: Electron.WebContents, options: Record<string, any>) {
if (options.webPreferences == null) {
options.webPreferences = {};
}
if (embedder.browserWindowOptions != null) {
let parentOptions = embedder.browserWindowOptions;
// if parent's visibility is available, that overrides 'show' flag (#12125)
const win = BrowserWindow.fromWebContents(embedder);
if (win != null) {
parentOptions = {
...win.getBounds(),
...embedder.browserWindowOptions,
show: win.isVisible()
};
}
// Inherit the original options if it is a BrowserWindow.
mergeOptions(options, parentOptions);
} else {
// Or only inherit webPreferences if it is a webview.
mergeOptions(options.webPreferences, embedder.getLastWebPreferences());
}
// Inherit certain option values from parent window
const webPreferences = embedder.getLastWebPreferences();
for (const [name, value] of inheritedWebPreferences) {
if ((webPreferences as any)[name] === value) {
options.webPreferences[name] = value;
}
}
if (!webPreferences.nativeWindowOpen) {
// Sets correct openerId here to give correct options to 'new-window' event handler
options.webPreferences.openerId = embedder.id;
}
return options;
};
const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';
function makeContentTypeHeader ({ contentType, boundary }: { contentType: string, boundary?: string }) {
const header = `content-type: ${contentType};`;
if (contentType === MULTIPART_CONTENT_TYPE) {
return `${header} boundary=${boundary}`;
}
return header;
type PostData = LoadURLOptions['postData']
export type WindowOpenArgs = {
url: string,
frameName: string,
features: string,
}
// Figure out appropriate headers for post data.
const parseContentTypeFormat = function (postData: Electron.UploadRawData[]) {
if (postData.length) {
// For multipart forms, the first element will start with the boundary
// notice, which looks something like `------WebKitFormBoundary12345678`
// Note, this regex would fail when submitting a urlencoded form with an
// input attribute of name="--theKey", but, uhh, don't do that?
const postDataFront = postData[0].bytes.toString();
const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
if (boundary) {
return {
boundary: boundary[0].substr(2),
contentType: MULTIPART_CONTENT_TYPE
};
}
}
// Either the form submission didn't contain any inputs (the postData array
// was empty), or we couldn't find the boundary and thus we can assume this is
// a key=value style form.
return {
contentType: URL_ENCODED_CONTENT_TYPE
};
};
const frameNamesToWindow = new Map<string, BrowserWindow>();
const registerFrameNameToGuestWindow = (name: string, win: BrowserWindow) => frameNamesToWindow.set(name, win);
const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
const getGuestWindowByFrameName = (name: string) => frameNamesToWindow.get(name);
// Setup a new guest with |embedder|
const setupGuest = function (embedder: Electron.WebContents, frameName: string, guest: Electron.BrowserWindow) {
// When |embedder| is destroyed we should also destroy attached guest, and if
// guest is closed by user then we should prevent |embedder| from double
// closing guest.
const guestId = guest.webContents.id;
/**
* `openGuestWindow` is called for both implementations of window.open
* (BrowserWindowProxy and nativeWindowOpen) to create and setup event handling
* for the new window.
*
* Until its removal in 12.0.0, the `new-window` event is fired, allowing the
* user to preventDefault() on the passed event (which ends up calling
* DestroyWebContents in the nativeWindowOpen code path).
*/
export function openGuestWindow ({ event, embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs }: {
event: { sender: WebContents, defaultPrevented: boolean },
embedder: WebContents,
guest?: WebContents,
referrer: Referrer,
disposition: string,
postData?: PostData,
overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
windowOpenArgs: WindowOpenArgs,
}): BrowserWindow | undefined {
const { url, frameName, features } = windowOpenArgs;
const isNativeWindowOpen = !!guest;
const { options: browserWindowOptions, additionalFeatures } = makeBrowserWindowOptions({
embedder,
features,
frameName,
overrideOptions: overrideBrowserWindowOptions
});
const didCancelEvent = emitDeprecatedNewWindowEvent({
event,
embedder,
guest,
browserWindowOptions,
windowOpenArgs,
additionalFeatures,
disposition,
referrer
});
if (didCancelEvent) return;
// To spec, subsequent window.open calls with the same frame name (`target` in
// spec parlance) will reuse the previous window.
// https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
const existingWindow = getGuestWindowByFrameName(frameName);
if (existingWindow) {
existingWindow.loadURL(url);
return existingWindow;
}
const window = new BrowserWindow({
webContents: guest,
...browserWindowOptions
});
if (!isNativeWindowOpen) {
// We should only call `loadURL` if the webContents was constructed by us in
// the case of BrowserWindowProxy (non-sandboxed, nativeWindowOpen: false),
// as navigating to the url when creating the window from an existing
// webContents is not necessary (it will navigate there anyway).
window.loadURL(url, {
httpReferrer: referrer,
...(postData && {
postData,
extraHeaders: formatPostDataHeaders(postData)
})
});
}
handleWindowLifecycleEvents({ embedder, frameName, guest: window });
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, additionalFeatures, referrer, postData });
return window;
}
/**
* Manage the relationship between embedder window and guest window. When the
* guest is destroyed, notify the embedder. When the embedder is destroyed, so
* too is the guest destroyed; this is Electron convention and isn't based in
* browser behavior.
*/
const handleWindowLifecycleEvents = function ({ embedder, guest, frameName }: {
embedder: WebContents,
guest: BrowserWindow,
frameName: string
}) {
const closedByEmbedder = function () {
guest.removeListener('closed', closedByUser);
guest.destroy();
};
const cachedGuestId = guest.webContents.id;
const closedByUser = function () {
embedder._sendInternal(`${IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_CLOSED}_${guestId}`);
embedder._sendInternal(`${IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_CLOSED}_${cachedGuestId}`);
embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
};
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
guest.once('closed', closedByUser);
if (frameName) {
frameToGuest.set(frameName, guest);
guest.frameName = frameName;
registerFrameNameToGuestWindow(frameName, guest);
guest.once('closed', function () {
frameToGuest.delete(frameName);
unregisterFrameName(frameName);
});
}
return guestId;
};
// Create a new guest created by |embedder| with |options|.
const createGuest = function (embedder: Electron.webContents, url: string, referrer: string | Electron.Referrer,
frameName: string, options: Record<string, any>, postData?: Electron.UploadRawData[]) {
let guest = frameToGuest.get(frameName);
if (frameName && (guest != null)) {
guest.loadURL(url);
return guest.webContents.id;
}
// Remember the embedder window's id.
if (options.webPreferences == null) {
options.webPreferences = {};
}
guest = new BrowserWindow(options);
if (!options.webContents) {
// We should not call `loadURL` if the window was constructed from an
// existing webContents (window.open in a sandboxed renderer).
//
// Navigating to the url when creating the window from an existing
// webContents is not necessary (it will navigate there anyway).
const loadOptions: Electron.LoadURLOptions = {
httpReferrer: referrer
};
if (postData != null) {
loadOptions.postData = postData;
loadOptions.extraHeaders = makeContentTypeHeader(parseContentTypeFormat(postData));
}
guest.loadURL(url, loadOptions);
}
return setupGuest(embedder, frameName, guest);
};
const getGuestWindow = function (guestContents: Electron.WebContents) {
let guestWindow = BrowserWindow.fromWebContents(guestContents);
if (guestWindow == null) {
const hostContents = guestContents.hostWebContents;
if (hostContents != null) {
guestWindow = BrowserWindow.fromWebContents(hostContents);
}
}
if (!guestWindow) {
throw new Error('getGuestWindow failed');
}
return guestWindow;
};
const isChildWindow = function (sender: Electron.WebContents, target: Electron.WebContents) {
return target.getLastWebPreferences().openerId === sender.id;
};
const isRelatedWindow = function (sender: Electron.WebContents, target: Electron.WebContents) {
return isChildWindow(sender, target) || isChildWindow(target, sender);
};
const isScriptableWindow = function (sender: Electron.WebContents, target: Electron.WebContents) {
return isRelatedWindow(sender, target) && isSameOrigin(sender.getURL(), target.getURL());
};
const isNodeIntegrationEnabled = function (sender: Electron.WebContents) {
return sender.getLastWebPreferences().nodeIntegration === true;
};
// Checks whether |sender| can access the |target|:
const canAccessWindow = function (sender: Electron.WebContents, target: Electron.WebContents) {
return isChildWindow(sender, target) ||
isScriptableWindow(sender, target) ||
isNodeIntegrationEnabled(sender);
};
// Routed window.open messages with raw options
ipcMainInternal.on(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_OPEN, (event, url: string, frameName: string, features: string) => {
// This should only be allowed for senders that have nativeWindowOpen: false
const lastWebPreferences = event.sender.getLastWebPreferences();
if (lastWebPreferences.nativeWindowOpen || lastWebPreferences.sandbox) {
event.returnValue = null;
throw new Error(`${IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_OPEN} denied: expected native window.open`);
}
if (url == null || url === '') url = 'about:blank';
if (frameName == null) frameName = '';
if (features == null) features = '';
const disposition = 'new-window';
const { options, webPreferences, additionalFeatures } = parseFeatures(features);
if (!options.title) options.title = frameName;
(options as Electron.BrowserWindowConstructorOptions).webPreferences = webPreferences;
const referrer: Electron.Referrer = { url: '', policy: 'default' };
internalWindowOpen(event, url, referrer, frameName, disposition, options, additionalFeatures);
});
// Routed window.open messages with fully parsed options
export function internalWindowOpen (event: ElectronInternal.IpcMainInternalEvent, url: string, referrer: string | Electron.Referrer,
frameName: string, disposition: string, options: Record<string, any>, additionalFeatures: string[], postData?: Electron.UploadRawData[]) {
options = mergeBrowserWindowOptions(event.sender, options);
/**
* Deprecated in favor of `webContents.setWindowOpenHandler` and
* `did-create-window` in 11.0.0. Will be removed in 12.0.0.
*/
function emitDeprecatedNewWindowEvent ({ event, embedder, guest, windowOpenArgs, browserWindowOptions, additionalFeatures, disposition, referrer, postData }: {
event: { sender: WebContents, defaultPrevented: boolean },
embedder: WebContents,
guest?: WebContents,
windowOpenArgs: WindowOpenArgs,
browserWindowOptions: BrowserWindowConstructorOptions,
additionalFeatures: string[]
disposition: string,
referrer: Referrer,
postData?: PostData,
}): boolean {
const { url, frameName } = windowOpenArgs;
const isWebViewWithPopupsDisabled = embedder.getType() === 'webview' && (embedder as any).getLastWebPreferences().disablePopups;
const postBody = postData ? {
data: postData,
...parseContentTypeFormat(postData)
headers: formatPostDataHeaders(postData)
} : null;
event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures, referrer, postBody);
const { newGuest } = event as unknown as { newGuest: Electron.BrowserWindow };
if ((event.sender.getType() === 'webview' && event.sender.getLastWebPreferences().disablePopups) || event.defaultPrevented) {
if (newGuest != null) {
if (options.webContents === newGuest.webContents) {
// the webContents is not changed, so set defaultPrevented to false to
embedder.emit(
'new-window',
event,
url,
frameName,
disposition,
{
...browserWindowOptions,
webContents: guest
},
additionalFeatures,
referrer,
postBody
);
const { newGuest } = event as any;
if (isWebViewWithPopupsDisabled) return true;
if (event.defaultPrevented) {
if (newGuest) {
if (guest === newGuest.webContents) {
// The webContents is not changed, so set defaultPrevented to false to
// stop the callers of this event from destroying the webContents.
(event as any).defaultPrevented = false;
}
event.returnValue = setupGuest(event.sender, frameName, newGuest);
} else {
event.returnValue = null;
handleWindowLifecycleEvents({
embedder: event.sender,
guest: newGuest,
frameName
});
}
} else {
event.returnValue = createGuest(event.sender, url, referrer, frameName, options, postData);
return true;
}
return false;
}
const makeSafeHandler = function<Event> (handler: (event: Event, guestContents: Electron.webContents, ...args: any[]) => any) {
return (event: Event, guestId: number, ...args: any[]) => {
// Access webContents via electron to prevent circular require.
const guestContents = electron.webContents.fromId(guestId);
if (!guestContents) {
throw new Error(`Invalid guestId: ${guestId}`);
// Security options that child windows will always inherit from parent windows
const securityWebPreferences: { [key: string]: boolean } = {
contextIsolation: true,
javascript: false,
nativeWindowOpen: true,
nodeIntegration: false,
enableRemoteModule: false,
sandbox: true,
webviewTag: false,
nodeIntegrationInSubFrames: false,
enableWebSQL: false
};
function makeBrowserWindowOptions ({ embedder, features, frameName, overrideOptions, useDeprecatedBehaviorForBareValues = true, useDeprecatedBehaviorForOptionInheritance = true }: {
embedder: WebContents,
features: string,
frameName: string,
overrideOptions?: BrowserWindowConstructorOptions,
useDeprecatedBehaviorForBareValues?: boolean
useDeprecatedBehaviorForOptionInheritance?: boolean
}) {
const { options: parsedOptions, webPreferences: parsedWebPreferences, additionalFeatures } = parseFeatures(features, useDeprecatedBehaviorForBareValues);
const deprecatedInheritedOptions = getDeprecatedInheritedOptions(embedder);
return {
additionalFeatures,
options: {
...(useDeprecatedBehaviorForOptionInheritance && deprecatedInheritedOptions),
show: true,
title: frameName,
width: 800,
height: 600,
...parsedOptions,
...overrideOptions,
webPreferences: makeWebPreferences({ embedder, insecureParsedWebPreferences: parsedWebPreferences, secureOverrideWebPreferences: overrideOptions && overrideOptions.webPreferences, useDeprecatedBehaviorForOptionInheritance: true })
}
return handler(event, guestContents, ...args);
};
};
}
const handleMessage = function (channel: string, handler: (event: Electron.IpcMainInvokeEvent, guestContents: Electron.webContents, ...args: any[]) => any) {
ipcMainInternal.handle(channel, makeSafeHandler(handler));
};
export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {}, useDeprecatedBehaviorForOptionInheritance = true }: {
embedder: WebContents,
insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'],
// Note that override preferences are considered elevated, and should only be
// sourced from the main process, as they override security defaults. If you
// have unvetted prefs, use parsedWebPreferences.
secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'],
useDeprecatedBehaviorForBareValues?: boolean
useDeprecatedBehaviorForOptionInheritance?: boolean
}) {
const deprecatedInheritedOptions = getDeprecatedInheritedOptions(embedder);
const parentWebPreferences = (embedder as any).getLastWebPreferences();
const securityWebPreferencesFromParent = Object.keys(securityWebPreferences).reduce((map, key) => {
if (securityWebPreferences[key] === parentWebPreferences[key]) {
map[key] = parentWebPreferences[key];
}
return map;
}, {} as any);
const openerId = parentWebPreferences.nativeWindowOpen ? null : embedder.id;
const handleMessageSync = function (channel: string, handler: (event: ElectronInternal.IpcMainInternalEvent, guestContents: Electron.webContents, ...args: any[]) => any) {
ipcMainUtils.handleSync(channel, makeSafeHandler(handler));
};
return {
...(useDeprecatedBehaviorForOptionInheritance && deprecatedInheritedOptions ? deprecatedInheritedOptions.webPreferences : null),
...parsedWebPreferences,
// Note that order is key here, we want to disallow the renderer's
// ability to change important security options but allow main (via
// setWindowOpenHandler) to change them.
...securityWebPreferencesFromParent,
...secureOverrideWebPreferences,
// Sets correct openerId here to give correct options to 'new-window' event handler
// TODO: Figure out another way to pass this?
openerId
};
}
const securityCheck = function (contents: Electron.WebContents, guestContents: Electron.WebContents, check: (sender: Electron.WebContents, target: Electron.WebContents) => boolean) {
if (!check(contents, guestContents)) {
console.error(`Blocked ${contents.getURL()} from accessing guestId: ${guestContents.id}`);
throw new Error(`Access denied to guestId: ${guestContents.id}`);
}
};
const windowMethods = new Set([
'destroy',
'focus',
'blur'
]);
handleMessage(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_METHOD, (event, guestContents, method: string, ...args: any[]) => {
securityCheck(event.sender, guestContents, canAccessWindow);
if (!windowMethods.has(method)) {
console.error(`Blocked ${event.sender.getURL()} from calling method: ${method}`);
throw new Error(`Invalid method: ${method}`);
/**
* Current Electron behavior is to inherit all options from the parent window.
* In practical use, this is kind of annoying because consumers have to know
* about the parent window's preferences in order to unset them and makes child
* windows even more of an anomaly. In 11.0.0 we will remove this behavior and
* only critical security preferences will be inherited by default.
*/
function getDeprecatedInheritedOptions (embedder: WebContents) {
if (!(embedder as any).browserWindowOptions) {
// If it's a webview, return just the webPreferences.
return {
webPreferences: (embedder as any).getLastWebPreferences()
};
}
return (getGuestWindow(guestContents) as any)[method](...args);
});
const { type, show, ...inheritableOptions } = (embedder as any).browserWindowOptions;
return inheritableOptions;
}
handleMessage(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE, (event, guestContents, message, targetOrigin, sourceOrigin) => {
if (targetOrigin == null) {
targetOrigin = '*';
function formatPostDataHeaders (postData: any) {
if (!postData) return;
let extraHeaders = 'content-type: application/x-www-form-urlencoded';
if (postData.length > 0) {
const postDataFront = postData[0].bytes.toString();
const boundary = /^--.*[^-\r\n]/.exec(
postDataFront
);
if (boundary != null) {
extraHeaders = `content-type: multipart/form-data; boundary=${boundary[0].substr(
2
)}`;
}
}
// The W3C does not seem to have word on how postMessage should work when the
// origins do not match, so we do not do |canAccessWindow| check here since
// postMessage across origins is useful and not harmful.
securityCheck(event.sender, guestContents, isRelatedWindow);
if (targetOrigin === '*' || isSameOrigin(guestContents.getURL(), targetOrigin)) {
const sourceId = event.sender.id;
guestContents._sendInternal(IPC_MESSAGES.GUEST_WINDOW_POSTMESSAGE, sourceId, message, sourceOrigin);
}
});
const webContentsMethodsAsync = new Set([
'loadURL',
'executeJavaScript',
'print'
]);
handleMessage(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD, (event, guestContents, method: string, ...args: any[]) => {
securityCheck(event.sender, guestContents, canAccessWindow);
if (!webContentsMethodsAsync.has(method)) {
console.error(`Blocked ${event.sender.getURL()} from calling method: ${method}`);
throw new Error(`Invalid method: ${method}`);
}
return (guestContents as any)[method](...args);
});
const webContentsMethodsSync = new Set([
'getURL'
]);
handleMessageSync(IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD, (event, guestContents, method: string, ...args: any[]) => {
securityCheck(event.sender, guestContents, canAccessWindow);
if (!webContentsMethodsSync.has(method)) {
console.error(`Blocked ${event.sender.getURL()} from calling method: ${method}`);
throw new Error(`Invalid method: ${method}`);
}
return (guestContents as any)[method](...args);
});
return extraHeaders;
}

View file

@ -0,0 +1,211 @@
/**
* Manage guest windows when using the default BrowserWindowProxy version of the
* renderer's window.open (i.e. nativeWindowOpen off). This module mostly
* consists of marshaling IPC requests from the BrowserWindowProxy to the
* WebContents.
*/
import { webContents, BrowserWindow } from 'electron/main';
import type { WebContents } from 'electron/main';
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
import { openGuestWindow } from '@electron/internal/browser/guest-window-manager';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
const { isSameOrigin } = process._linkedBinding('electron_common_v8_util');
const getGuestWindow = function (guestContents: WebContents) {
let guestWindow = BrowserWindow.fromWebContents(guestContents);
if (guestWindow == null) {
const hostContents = guestContents.hostWebContents;
if (hostContents != null) {
guestWindow = BrowserWindow.fromWebContents(hostContents);
}
}
if (!guestWindow) {
throw new Error('getGuestWindow failed');
}
return guestWindow;
};
const isChildWindow = function (sender: WebContents, target: WebContents) {
return (target as any).getLastWebPreferences().openerId === sender.id;
};
const isRelatedWindow = function (sender: WebContents, target: WebContents) {
return isChildWindow(sender, target) || isChildWindow(target, sender);
};
const isScriptableWindow = function (sender: WebContents, target: WebContents) {
return (
isRelatedWindow(sender, target) &&
isSameOrigin(sender.getURL(), target.getURL())
);
};
const isNodeIntegrationEnabled = function (sender: WebContents) {
return (sender as any).getLastWebPreferences().nodeIntegration === true;
};
// Checks whether |sender| can access the |target|:
const canAccessWindow = function (sender: WebContents, target: WebContents) {
return (
isChildWindow(sender, target) ||
isScriptableWindow(sender, target) ||
isNodeIntegrationEnabled(sender)
);
};
// Routed window.open messages with raw options
ipcMainInternal.on(
IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_OPEN,
(
event,
url: string,
frameName: string,
features: string
) => {
// This should only be allowed for senders that have nativeWindowOpen: false
const lastWebPreferences = (event.sender as any).getLastWebPreferences();
if (lastWebPreferences.nativeWindowOpen || lastWebPreferences.sandbox) {
(event as any).returnValue = null;
throw new Error(
'GUEST_WINDOW_MANAGER_WINDOW_OPEN denied: expected native window.open'
);
}
const browserWindowOptions = (event.sender as any)._callWindowOpenHandler(event, url, frameName, features);
if (event.defaultPrevented) {
return;
}
const guest = openGuestWindow({
event,
embedder: event.sender,
referrer: { url: '', policy: 'default' },
disposition: 'new-window',
overrideBrowserWindowOptions: browserWindowOptions,
windowOpenArgs: {
url: url || 'about:blank',
frameName: frameName || '',
features: features || ''
}
});
(event as any).returnValue = guest ? guest.webContents.id : null;
}
);
type IpcHandler<T, Event> = (event: Event, guestContents: Electron.WebContents, ...args: any[]) => T;
const makeSafeHandler = function<T, Event> (handler: IpcHandler<T, Event>) {
return (event: Event, guestId: number, ...args: any[]) => {
// Access webContents via electron to prevent circular require.
const guestContents = webContents.fromId(guestId);
if (!guestContents) {
throw new Error(`Invalid guestId: ${guestId}`);
}
return handler(event, guestContents as Electron.WebContents, ...args);
};
};
const handleMessage = function (channel: string, handler: IpcHandler<any, Electron.IpcMainInvokeEvent>) {
ipcMainInternal.handle(channel, makeSafeHandler(handler));
};
const handleMessageSync = function (channel: string, handler: IpcHandler<any, ElectronInternal.IpcMainInternalEvent>) {
ipcMainUtils.handleSync(channel, makeSafeHandler(handler));
};
type ContentsCheck = (contents: WebContents, guestContents: WebContents) => boolean;
const securityCheck = function (contents: WebContents, guestContents: WebContents, check: ContentsCheck) {
if (!check(contents, guestContents)) {
console.error(
`Blocked ${contents.getURL()} from accessing guestId: ${guestContents.id}`
);
throw new Error(`Access denied to guestId: ${guestContents.id}`);
}
};
const windowMethods = new Set(['destroy', 'focus', 'blur']);
handleMessage(
IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_METHOD,
(event, guestContents, method, ...args) => {
securityCheck(event.sender, guestContents, canAccessWindow);
if (!windowMethods.has(method)) {
console.error(
`Blocked ${event.sender.getURL()} from calling method: ${method}`
);
throw new Error(`Invalid method: ${method}`);
}
return (getGuestWindow(guestContents) as any)[method](...args);
}
);
handleMessage(
IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE,
(event, guestContents, message, targetOrigin, sourceOrigin) => {
if (targetOrigin == null) {
targetOrigin = '*';
}
// The W3C does not seem to have word on how postMessage should work when the
// origins do not match, so we do not do |canAccessWindow| check here since
// postMessage across origins is useful and not harmful.
securityCheck(event.sender, guestContents, isRelatedWindow);
if (
targetOrigin === '*' ||
isSameOrigin(guestContents.getURL(), targetOrigin)
) {
const sourceId = event.sender.id;
guestContents._sendInternal(
IPC_MESSAGES.GUEST_WINDOW_POSTMESSAGE,
sourceId,
message,
sourceOrigin
);
}
}
);
const webContentsMethodsAsync = new Set([
'loadURL',
'executeJavaScript',
'print'
]);
handleMessage(
IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD,
(event, guestContents, method, ...args) => {
securityCheck(event.sender, guestContents, canAccessWindow);
if (!webContentsMethodsAsync.has(method)) {
console.error(
`Blocked ${event.sender.getURL()} from calling method: ${method}`
);
throw new Error(`Invalid method: ${method}`);
}
return (guestContents as any)[method](...args);
}
);
const webContentsMethodsSync = new Set(['getURL']);
handleMessageSync(
IPC_MESSAGES.GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD,
(event, guestContents, method, ...args) => {
securityCheck(event.sender, guestContents, canAccessWindow);
if (!webContentsMethodsSync.has(method)) {
console.error(
`Blocked ${event.sender.getURL()} from calling method: ${method}`
);
throw new Error(`Invalid method: ${method}`);
}
return (guestContents as any)[method](...args);
}
);

View file

@ -76,7 +76,7 @@ require('@electron/internal/browser/rpc-server');
// Load the guest view manager.
require('@electron/internal/browser/guest-view-manager');
require('@electron/internal/browser/guest-window-manager');
require('@electron/internal/browser/guest-window-proxy');
// Now we try to load app's package.json.
let packagePath = null;

View file

@ -123,11 +123,13 @@ export class NavigationController extends EventEmitter {
this.webContents.removeListener('did-fail-load', failListener);
this.webContents.removeListener('did-start-navigation', navigationListener);
this.webContents.removeListener('did-stop-loading', stopLoadingListener);
this.webContents.removeListener('destroyed', stopLoadingListener);
};
this.webContents.on('did-finish-load', finishListener);
this.webContents.on('did-fail-load', failListener);
this.webContents.on('did-start-navigation', navigationListener);
this.webContents.on('did-stop-loading', stopLoadingListener);
this.webContents.on('destroyed', stopLoadingListener);
});
// Add a no-op rejection handler to silence the unhandled rejection error.
p.catch(() => {});