electron/lib/browser/guest-window-manager.ts

297 lines
11 KiB
TypeScript
Raw Normal View History

/**
* 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';
2016-03-18 18:51:02 +00:00
type PostData = LoadURLOptions['postData']
export type WindowOpenArgs = {
url: string,
frameName: string,
features: string,
}
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);
/**
* `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,
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;
2016-01-12 02:40:23 +00:00
}
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 as Electron.UploadRawData[])
})
});
}
handleWindowLifecycleEvents({ embedder, frameName, guest: window });
embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, additionalFeatures, referrer, postData });
2016-01-12 02:40:23 +00:00
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 () {
2020-03-20 20:28:31 +00:00
guest.removeListener('closed', closedByUser);
guest.destroy();
};
const cachedGuestId = guest.webContents.id;
const closedByUser = function () {
embedder._sendInternal(`${IPC_MESSAGES.GUEST_WINDOW_MANAGER_WINDOW_CLOSED}_${cachedGuestId}`);
embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
2020-03-20 20:28:31 +00:00
};
embedder.once('current-render-view-deleted' as any, closedByEmbedder);
2020-03-20 20:28:31 +00:00
guest.once('closed', closedByUser);
2016-01-12 02:40:23 +00:00
if (frameName) {
registerFrameNameToGuestWindow(frameName, guest);
guest.once('closed', function () {
unregisterFrameName(frameName);
2020-03-20 20:28:31 +00:00
});
2016-01-12 02:40:23 +00:00
}
2020-03-20 20:28:31 +00:00
};
2016-01-12 02:40:23 +00:00
/**
* 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, newGuest?: BrowserWindow },
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.getLastWebPreferences().disablePopups;
const postBody = postData ? {
data: postData,
headers: formatPostDataHeaders(postData as Electron.UploadRawData[])
} : null;
embedder.emit(
'new-window',
event,
url,
frameName,
disposition,
{
...browserWindowOptions,
webContents: guest
},
additionalFeatures,
referrer,
postBody
);
const { newGuest } = event;
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.defaultPrevented = false;
}
handleWindowLifecycleEvents({
embedder: event.sender,
guest: newGuest,
frameName
});
2016-09-29 12:41:35 +00:00
}
return true;
2016-01-12 02:40:23 +00:00
}
return false;
}
2016-01-12 02:40:23 +00:00
// Security options that child windows will always inherit from parent windows
const securityWebPreferences: { [key: string]: boolean } = {
contextIsolation: true,
javascript: false,
nativeWindowOpen: true,
nodeIntegration: false,
sandbox: true,
webviewTag: false,
nodeIntegrationInSubFrames: false,
enableWebSQL: false
2020-03-20 20:28:31 +00:00
};
2016-01-12 02:40:23 +00:00
function makeBrowserWindowOptions ({ embedder, features, overrideOptions, useDeprecatedBehaviorForBareValues = true, useDeprecatedBehaviorForOptionInheritance = true }: {
embedder: WebContents,
features: string,
overrideOptions?: BrowserWindowConstructorOptions,
useDeprecatedBehaviorForBareValues?: boolean
useDeprecatedBehaviorForOptionInheritance?: boolean
}) {
const { options: parsedOptions, webPreferences: parsedWebPreferences, additionalFeatures } = parseFeatures(features, useDeprecatedBehaviorForBareValues);
2016-01-12 02:40:23 +00:00
const deprecatedInheritedOptions = getDeprecatedInheritedOptions(embedder);
return {
additionalFeatures,
options: {
...(useDeprecatedBehaviorForOptionInheritance && deprecatedInheritedOptions),
show: true,
width: 800,
height: 600,
...parsedOptions,
...overrideOptions,
webPreferences: makeWebPreferences({ embedder, insecureParsedWebPreferences: parsedWebPreferences, secureOverrideWebPreferences: overrideOptions && overrideOptions.webPreferences, useDeprecatedBehaviorForOptionInheritance: true })
} as Electron.BrowserViewConstructorOptions
};
}
2016-01-12 02:40:23 +00:00
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.getLastWebPreferences();
const securityWebPreferencesFromParent = (Object.keys(securityWebPreferences).reduce((map, key) => {
if (securityWebPreferences[key] === parentWebPreferences[key as keyof Electron.WebPreferences]) {
(map as any)[key] = parentWebPreferences[key as keyof Electron.WebPreferences];
}
return map;
}, {} as Electron.WebPreferences));
const openerId = parentWebPreferences.nativeWindowOpen ? null : embedder.id;
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
};
}
/**
* 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.browserWindowOptions) {
// If it's a webview, return just the webPreferences.
return {
webPreferences: embedder.getLastWebPreferences()
};
}
const { type, show, ...inheritableOptions } = embedder.browserWindowOptions;
return inheritableOptions;
}
function formatPostDataHeaders (postData: Electron.UploadRawData[]) {
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
)}`;
}
}
return extraHeaders;
}