guest-window-manager.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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, deprecate } 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. * Until its removal in 12.0.0, the `new-window` event is fired, allowing the
  26. * user to preventDefault() on the passed event (which ends up calling
  27. * DestroyWebContents).
  28. */
  29. export function openGuestWindow ({ event, embedder, guest, referrer, disposition, postData, overrideBrowserWindowOptions, windowOpenArgs, outlivesOpener }: {
  30. event: { sender: WebContents, defaultPrevented: boolean },
  31. embedder: WebContents,
  32. guest?: WebContents,
  33. referrer: Referrer,
  34. disposition: string,
  35. postData?: PostData,
  36. overrideBrowserWindowOptions?: BrowserWindowConstructorOptions,
  37. windowOpenArgs: WindowOpenArgs,
  38. outlivesOpener: boolean,
  39. }): BrowserWindow | undefined {
  40. const { url, frameName, features } = windowOpenArgs;
  41. const browserWindowOptions = makeBrowserWindowOptions({
  42. embedder,
  43. features,
  44. overrideOptions: overrideBrowserWindowOptions
  45. });
  46. const didCancelEvent = emitDeprecatedNewWindowEvent({
  47. event,
  48. embedder,
  49. guest,
  50. browserWindowOptions,
  51. windowOpenArgs,
  52. disposition,
  53. postData,
  54. referrer
  55. });
  56. if (didCancelEvent) return;
  57. // To spec, subsequent window.open calls with the same frame name (`target` in
  58. // spec parlance) will reuse the previous window.
  59. // https://html.spec.whatwg.org/multipage/window-object.html#apis-for-creating-and-navigating-browsing-contexts-by-name
  60. const existingWindow = getGuestWindowByFrameName(frameName);
  61. if (existingWindow) {
  62. if (existingWindow.isDestroyed() || existingWindow.webContents.isDestroyed()) {
  63. // FIXME(t57ser): The webContents is destroyed for some reason, unregister the frame name
  64. unregisterFrameName(frameName);
  65. } else {
  66. existingWindow.loadURL(url);
  67. return existingWindow;
  68. }
  69. }
  70. const window = new BrowserWindow({
  71. webContents: guest,
  72. ...browserWindowOptions
  73. });
  74. if (!guest) {
  75. // When we open a new window from a link (via OpenURLFromTab),
  76. // the browser process is responsible for initiating navigation
  77. // in the new window.
  78. window.loadURL(url, {
  79. httpReferrer: referrer,
  80. ...(postData && {
  81. postData,
  82. extraHeaders: formatPostDataHeaders(postData as Electron.UploadRawData[])
  83. })
  84. });
  85. }
  86. handleWindowLifecycleEvents({ embedder, frameName, guest: window, outlivesOpener });
  87. embedder.emit('did-create-window', window, { url, frameName, options: browserWindowOptions, disposition, referrer, postData });
  88. return window;
  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: BrowserWindow,
  99. frameName: string,
  100. outlivesOpener: boolean
  101. }) {
  102. const closedByEmbedder = function () {
  103. guest.removeListener('closed', 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('closed', closedByUser);
  116. if (frameName) {
  117. registerFrameNameToGuestWindow(frameName, guest);
  118. guest.once('closed', function () {
  119. unregisterFrameName(frameName);
  120. });
  121. }
  122. };
  123. /**
  124. * Deprecated in favor of `webContents.setWindowOpenHandler` and
  125. * `did-create-window` in 11.0.0. Will be removed in 12.0.0.
  126. */
  127. function emitDeprecatedNewWindowEvent ({ event, embedder, guest, windowOpenArgs, browserWindowOptions, disposition, referrer, postData }: {
  128. event: { sender: WebContents, defaultPrevented: boolean, newGuest?: BrowserWindow },
  129. embedder: WebContents,
  130. guest?: WebContents,
  131. windowOpenArgs: WindowOpenArgs,
  132. browserWindowOptions: BrowserWindowConstructorOptions,
  133. disposition: string,
  134. referrer: Referrer,
  135. postData?: PostData,
  136. }): boolean {
  137. const { url, frameName } = windowOpenArgs;
  138. const isWebViewWithPopupsDisabled = embedder.getType() === 'webview' && embedder.getLastWebPreferences()!.disablePopups;
  139. const postBody = postData ? {
  140. data: postData,
  141. ...parseContentTypeFormat(postData)
  142. } : null;
  143. if (embedder.listenerCount('new-window') > 0) {
  144. deprecate.log('The new-window event is deprecated and will be removed. Please use contents.setWindowOpenHandler() instead.');
  145. }
  146. embedder.emit(
  147. 'new-window',
  148. event,
  149. url,
  150. frameName,
  151. disposition,
  152. {
  153. ...browserWindowOptions,
  154. webContents: guest
  155. },
  156. [], // additionalFeatures
  157. referrer,
  158. postBody
  159. );
  160. const { newGuest } = event;
  161. if (isWebViewWithPopupsDisabled) return true;
  162. if (event.defaultPrevented) {
  163. if (newGuest) {
  164. if (guest === newGuest.webContents) {
  165. // The webContents is not changed, so set defaultPrevented to false to
  166. // stop the callers of this event from destroying the webContents.
  167. event.defaultPrevented = false;
  168. }
  169. handleWindowLifecycleEvents({
  170. embedder: event.sender,
  171. guest: newGuest,
  172. frameName,
  173. outlivesOpener: false
  174. });
  175. }
  176. return true;
  177. }
  178. return false;
  179. }
  180. // Security options that child windows will always inherit from parent windows
  181. const securityWebPreferences: { [key: string]: boolean } = {
  182. contextIsolation: true,
  183. javascript: false,
  184. nodeIntegration: false,
  185. sandbox: true,
  186. webviewTag: false,
  187. nodeIntegrationInSubFrames: false,
  188. enableWebSQL: false
  189. };
  190. function makeBrowserWindowOptions ({ embedder, features, overrideOptions }: {
  191. embedder: WebContents,
  192. features: string,
  193. overrideOptions?: BrowserWindowConstructorOptions,
  194. }) {
  195. const { options: parsedOptions, webPreferences: parsedWebPreferences } = parseFeatures(features);
  196. return {
  197. show: true,
  198. width: 800,
  199. height: 600,
  200. ...parsedOptions,
  201. ...overrideOptions,
  202. // Note that for normal code path an existing WebContents created by
  203. // Chromium will be used, with web preferences parsed in the
  204. // |-will-add-new-contents| event.
  205. // The |webPreferences| here is only used by the |new-window| event.
  206. webPreferences: makeWebPreferences({
  207. embedder,
  208. insecureParsedWebPreferences: parsedWebPreferences,
  209. secureOverrideWebPreferences: overrideOptions && overrideOptions.webPreferences
  210. })
  211. } as Electron.BrowserViewConstructorOptions;
  212. }
  213. export function makeWebPreferences ({ embedder, secureOverrideWebPreferences = {}, insecureParsedWebPreferences: parsedWebPreferences = {} }: {
  214. embedder: WebContents,
  215. insecureParsedWebPreferences?: ReturnType<typeof parseFeatures>['webPreferences'],
  216. // Note that override preferences are considered elevated, and should only be
  217. // sourced from the main process, as they override security defaults. If you
  218. // have unvetted prefs, use parsedWebPreferences.
  219. secureOverrideWebPreferences?: BrowserWindowConstructorOptions['webPreferences'],
  220. }) {
  221. const parentWebPreferences = embedder.getLastWebPreferences()!;
  222. const securityWebPreferencesFromParent = (Object.keys(securityWebPreferences).reduce((map, key) => {
  223. if (securityWebPreferences[key] === parentWebPreferences[key as keyof Electron.WebPreferences]) {
  224. (map as any)[key] = parentWebPreferences[key as keyof Electron.WebPreferences];
  225. }
  226. return map;
  227. }, {} as Electron.WebPreferences));
  228. return {
  229. ...parsedWebPreferences,
  230. // Note that order is key here, we want to disallow the renderer's
  231. // ability to change important security options but allow main (via
  232. // setWindowOpenHandler) to change them.
  233. ...securityWebPreferencesFromParent,
  234. ...secureOverrideWebPreferences
  235. };
  236. }
  237. function formatPostDataHeaders (postData: PostData) {
  238. if (!postData) return;
  239. const { contentType, boundary } = parseContentTypeFormat(postData);
  240. if (boundary != null) { return `content-type: ${contentType}; boundary=${boundary}`; }
  241. return `content-type: ${contentType}`;
  242. }
  243. const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
  244. const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';
  245. // Figure out appropriate headers for post data.
  246. export const parseContentTypeFormat = function (postData: Exclude<PostData, undefined>) {
  247. if (postData.length) {
  248. if (postData[0].type === 'rawData') {
  249. // For multipart forms, the first element will start with the boundary
  250. // notice, which looks something like `------WebKitFormBoundary12345678`
  251. // Note, this regex would fail when submitting a urlencoded form with an
  252. // input attribute of name="--theKey", but, uhh, don't do that?
  253. const postDataFront = postData[0].bytes.toString();
  254. const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
  255. if (boundary) {
  256. return {
  257. boundary: boundary[0].substr(2),
  258. contentType: MULTIPART_CONTENT_TYPE
  259. };
  260. }
  261. }
  262. }
  263. // Either the form submission didn't contain any inputs (the postData array
  264. // was empty), or we couldn't find the boundary and thus we can assume this is
  265. // a key=value style form.
  266. return {
  267. contentType: URL_ENCODED_CONTENT_TYPE
  268. };
  269. };