guest-window-manager.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * Create and minimally track guest windows at the direction of the renderer
  3. * (via window.open). Here, "guest" roughly means "child" — it's not necessarily
  4. * emblematic of its process status; both in-process (same-origin) and
  5. * out-of-process (cross-origin) are created here. "Embedder" roughly means
  6. * "parent."
  7. */
  8. import { BrowserWindow } from 'electron/main';
  9. import type { BrowserWindowConstructorOptions, Referrer, WebContents, LoadURLOptions } from 'electron/main';
  10. import { parseFeatures } from '@electron/internal/browser/parse-features-string';
  11. type PostData = LoadURLOptions['postData']
  12. export type WindowOpenArgs = {
  13. url: string,
  14. frameName: string,
  15. features: string,
  16. }
  17. const frameNamesToWindow = new Map<string, BrowserWindow>();
  18. const registerFrameNameToGuestWindow = (name: string, win: BrowserWindow) => frameNamesToWindow.set(name, win);
  19. const unregisterFrameName = (name: string) => frameNamesToWindow.delete(name);
  20. const getGuestWindowByFrameName = (name: string) => frameNamesToWindow.get(name);
  21. /**
  22. * `openGuestWindow` is called to create and setup event handling for the new
  23. * window.
  24. */
  25. export function openGuestWindow ({ embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
  26. embedder: WebContents,
  27. guest?: WebContents,
  28. referrer: Referrer,
  29. disposition: string,
  30. postData?: PostData,
  31. overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
  32. windowOpenArgs: WindowOpenArgs,
  33. outlivesOpener: boolean,
  34. }): BrowserWindow | undefined {
  35. const { url, frameName, features } = windowOpenArgs;
  36. const { options: parsedOptions } = parseFeatures(features);
  37. const browserWindowOptions = {
  38. show: true,
  39. width: 800,
  40. height: 600,
  41. ...parsedOptions,
  42. ...overrideBrowserWindowOptions
  43. };
  44. // To spec, subsequent window.open calls with the same frame name (`target` in
  45. // spec parlance) will reuse the previous window.
  46. // https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
  47. const existingWindow = getGuestWindowByFrameName(frameName);
  48. if (existingWindow) {
  49. if (existingWindow.isDestroyed() || existingWindow.webContents.isDestroyed()) {
  50. // FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
  51. unregisterFrameName(frameName);
  52. } else {
  53. existingWindow.loadURL(url);
  54. return existingWindow;
  55. }
  56. }
  57. const window = new BrowserWindow({
  58. webContents: guest,
  59. ...browserWindowOptions
  60. });
  61. if (!guest) {
  62. // When we open a new window from a link (via OpenURLFromTab),
  63. // the browser process is responsible for initiating navigation
  64. // in the new window.
  65. window.loadURL(url, {
  66. httpReferrer: referrer,
  67. ...(postData && {
  68. postData,
  69. extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
  70. })
  71. });
  72. }
  73. handleWindowLifecycleEvents({ embedder, frameName, guest: window, outlivesOpener });
  74. embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
  75. return window;
  76. }
  77. /**
  78. * Manage the relationship between embedder window and guest window. When the
  79. * guest is destroyed, notify the embedder. When the embedder is destroyed, so
  80. * too is the guest destroyed; this is Electron convention and isn't based in
  81. * browser behavior.
  82. */
  83. const handleWindowLifecycleEvents = function ({ embedder, guest, frameName, outlivesOpener }: {
  84. embedder: WebContents,
  85. guest: BrowserWindow,
  86. frameName: string,
  87. outlivesOpener: boolean
  88. }) {
  89. const closedByEmbedder = function () {
  90. guest.removeListener('closed', closedByUser);
  91. guest.destroy();
  92. };
  93. const closedByUser = function () {
  94. // Embedder might have been closed
  95. if (!embedder.isDestroyed() && !outlivesOpener) {
  96. embedder.removeListener('current-render-view-deleted' as any, closedByEmbedder);
  97. }
  98. };
  99. if (!outlivesOpener) {
  100. embedder.once('current-render-view-deleted' as any, closedByEmbedder);
  101. }
  102. guest.once('closed', closedByUser);
  103. if (frameName) {
  104. registerFrameNameToGuestWindow(frameName, guest);
  105. guest.once('closed', function () {
  106. unregisterFrameName(frameName);
  107. });
  108. }
  109. };
  110. // Security options that child windows will always inherit from parent windows
  111. const securityWebPreferences: { [key: string]: boolean } = {
  112. contextIsolation: true,
  113. javascript: false,
  114. nodeIntegration: false,
  115. sandbox: true,
  116. webviewTag: false,
  117. nodeIntegrationInSubFrames: false,
  118. enableWebSQL: false
  119. };
  120. export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {} }: {
  121. embedder: WebContents,
  122. insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'],
  123. // Note that override preferences are considered elevated, and should only be
  124. // sourced from the main process, as they override security defaults. If you
  125. // have unvetted prefs, use parsedWebPreferences.
  126. secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'],
  127. }) {
  128. const parentWebPreferences = embedder.getLastWebPreferences()!;
  129. const securityWebPreferencesFromParent = (Object.keys(securityWebPreferences).reduce((map, key) => {
  130. if (securityWebPreferences[key] === parentWebPreferences[key as keyof Electron.WebPreferences]) {
  131. (map as any)[key] = parentWebPreferences[key as keyof Electron.WebPreferences];
  132. }
  133. return map;
  134. }, {} as Electron.WebPreferences));
  135. return {
  136. ...parsedWebPreferences,
  137. // Note that order is key here, we want to disallow the renderer's
  138. // ability to change important security options but allow main (via
  139. // setWindowOpenHandler) to change them.
  140. ...securityWebPreferencesFromParent,
  141. ...secureOverrideWebPreferences
  142. };
  143. }
  144. function formatPostDataHeaders (postData: PostData) {
  145. if (!postData) return;
  146. const { contentType, boundary } = parseContentTypeFormat(postData);
  147. if (boundary != null) { return `content-type: ${contentType}; boundary=${boundary}`; }
  148. return `content-type: ${contentType}`;
  149. }
  150. const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
  151. const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';
  152. // Figure out appropriate headers for post data.
  153. export const parseContentTypeFormat = function (postData: Exclude<PostData, undefined>) {
  154. if (postData.length) {
  155. if (postData[0].type === 'rawData') {
  156. // For multipart forms, the first element will start with the boundary
  157. // notice, which looks something like `------WebKitFormBoundary12345678`
  158. // Note, this regex would fail when submitting a urlencoded form with an
  159. // input attribute of name="--theKey", but, uhh, don't do that?
  160. const postDataFront = postData[0].bytes.toString();
  161. const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
  162. if (boundary) {
  163. return {
  164. boundary: boundary[0].substr(2),
  165. contentType: MULTIPART_CONTENT_TYPE
  166. };
  167. }
  168. }
  169. }
  170. // Either the form submission didn't contain any inputs (the postData array
  171. // was empty), or we couldn't find the boundary and thus we can assume this is
  172. // a key=value style form.
  173. return {
  174. contentType: URL_ENCODED_CONTENT_TYPE
  175. };
  176. };