guest-window-manager.ts 7.6 KB

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