/** * 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'; type PostData = LoadURLOptions['postData'] export type WindowOpenArgs = { url: string, frameName: string, features: string, } const frameNamesToWindow = new Map(); 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; } 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 }); 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}_${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) { registerFrameNameToGuestWindow(frameName, guest); guest.once('closed', function () { unregisterFrameName(frameName); }); } }; /** * 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 }); } return true; } return false; } // 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 }; 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); 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 }; } export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {}, useDeprecatedBehaviorForOptionInheritance = true }: { embedder: WebContents, insecureParsedWebPreferences?: ReturnType['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; }