123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- /**
- * 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) and
- * out-of-process (cross-origin) are created here. "Embedder" roughly means
- * "parent."
- */
- import { BrowserWindow, deprecate } from 'electron/main';
- import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';
- import { parseFeatures } from '@electron/internal/browser/parse-features-string';
- 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 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).
- */
- export function openGuestWindow ({ event, embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
- event: { sender: WebContents, defaultPrevented: boolean },
- embedder: WebContents,
- guest?: WebContents,
- referrer: Referrer,
- disposition: string,
- postData?: PostData,
- overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
- windowOpenArgs: WindowOpenArgs,
- outlivesOpener: boolean,
- }): BrowserWindow | undefined {
- const { url, frameName, features } = windowOpenArgs;
- const browserWindowOptions = makeBrowserWindowOptions({
- embedder,
- features,
- overrideOptions: overrideBrowserWindowOptions
- });
- const didCancelEvent = emitDeprecatedNewWindowEvent({
- event,
- embedder,
- guest,
- browserWindowOptions,
- windowOpenArgs,
- disposition,
- postData,
- 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) {
- if (existingWindow.isDestroyed() || existingWindow.webContents.isDestroyed()) {
- // FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
- unregisterFrameName(frameName);
- } else {
- existingWindow.loadURL(url);
- return existingWindow;
- }
- }
- const window = new BrowserWindow({
- webContents: guest,
- ...browserWindowOptions
- });
- if (!guest) {
- // When we open a new window from a link (via OpenURLFromTab),
- // the browser process is responsible for initiating navigation
- // in the new window.
- window.loadURL(url, {
- httpReferrer: referrer,
- ...(postData && {
- postData,
- extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
- })
- });
- }
- handleWindowLifecycleEvents({ embedder, frameName, guest: window, outlivesOpener });
- embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, 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, outlivesOpener }: {
- embedder: WebContents,
- guest: BrowserWindow,
- frameName: string,
- outlivesOpener: boolean
- }) {
- const closedByEmbedder = function () {
- guest.removeListener('closed', closedByUser);
- guest.destroy();
- };
- const closedByUser = function () {
- // Embedder might have been closed
- if (!embedder.isDestroyed() && !outlivesOpener) {
- embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
- }
- };
- if (!outlivesOpener) {
- 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, disposition, referrer, postData }: {
- event: { sender: WebContents, defaultPrevented: boolean, newGuest?: BrowserWindow },
- embedder: WebContents,
- guest?: WebContents,
- windowOpenArgs: WindowOpenArgs,
- browserWindowOptions: BrowserWindowConstructorOptions,
- disposition: string,
- referrer: Referrer,
- postData?: PostData,
- }): boolean {
- const { url, frameName } = windowOpenArgs;
- const isWebViewWithPopupsDisabled = embedder.getType() === 'webview' && embedder.getLastWebPreferences()!.disablePopups;
- const postBody = postData ? {
- data: postData,
- ...parseContentTypeFormat(postData)
- } : null;
- if (embedder.listenerCount('new-window') > 0) {
- deprecate.log('The new-window event is deprecated and will be removed. Please use contents.setWindowOpenHandler() instead.');
- }
- 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,
- outlivesOpener: false
- });
- }
- return true;
- }
- return false;
- }
- // Security options that child windows will always inherit from parent windows
- const securityWebPreferences: { [key: string]: boolean } = {
- contextIsolation: true,
- javascript: false,
- nodeIntegration: false,
- sandbox: true,
- webviewTag: false,
- nodeIntegrationInSubFrames: false,
- enableWebSQL: false
- };
- function makeBrowserWindowOptions ({ embedder, features, overrideOptions }: {
- embedder: WebContents,
- features: string,
- overrideOptions?: BrowserWindowConstructorOptions,
- }) {
- const { options: parsedOptions, webPreferences: parsedWebPreferences } = parseFeatures(features);
- return {
- show: true,
- width: 800,
- height: 600,
- ...parsedOptions,
- ...overrideOptions,
- // Note that for normal code path an existing WebContents created by
- // Chromium will be used, with web preferences parsed in the
- // |-will-add-new-contents| event.
- // The |webPreferences| here is only used by the |new-window| event.
- webPreferences: makeWebPreferences({
- embedder,
- insecureParsedWebPreferences: parsedWebPreferences,
- secureOverrideWebPreferences: overrideOptions && overrideOptions.webPreferences
- })
- } as Electron.BrowserViewConstructorOptions;
- }
- export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {} }: {
- 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'],
- }) {
- 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));
- return {
- ...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
- };
- }
- function formatPostDataHeaders (postData: PostData) {
- if (!postData) return;
- const { contentType, boundary } = parseContentTypeFormat(postData);
- if (boundary != null) { return `content-type: ${contentType}; boundary=${boundary}`; }
- return `content-type: ${contentType}`;
- }
- const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
- const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';
- // Figure out appropriate headers for post data.
- export const parseContentTypeFormat = function (postData: Exclude<PostData, undefined>) {
- if (postData.length) {
- if (postData[0].type === 'rawData') {
- // 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
- };
- };
|