guest-window-manager.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. 'use strict'
  2. const { BrowserWindow, webContents } = require('electron')
  3. const { isSameOrigin } = process.atomBinding('v8_util')
  4. const ipcMain = require('@electron/internal/browser/ipc-main-internal')
  5. const parseFeaturesString = require('@electron/internal/common/parse-features-string')
  6. const hasProp = {}.hasOwnProperty
  7. const frameToGuest = new Map()
  8. // Security options that child windows will always inherit from parent windows
  9. const inheritedWebPreferences = new Map([
  10. ['contextIsolation', true],
  11. ['javascript', false],
  12. ['nativeWindowOpen', true],
  13. ['nodeIntegration', false],
  14. ['enableRemoteModule', false],
  15. ['sandbox', true],
  16. ['webviewTag', false]
  17. ])
  18. // Copy attribute of |parent| to |child| if it is not defined in |child|.
  19. const mergeOptions = function (child, parent, visited) {
  20. // Check for circular reference.
  21. if (visited == null) visited = new Set()
  22. if (visited.has(parent)) return
  23. visited.add(parent)
  24. for (const key in parent) {
  25. if (key === 'isBrowserView') continue
  26. if (!hasProp.call(parent, key)) continue
  27. if (key in child && key !== 'webPreferences') continue
  28. const value = parent[key]
  29. if (typeof value === 'object') {
  30. child[key] = mergeOptions(child[key] || {}, value, visited)
  31. } else {
  32. child[key] = value
  33. }
  34. }
  35. visited.delete(parent)
  36. return child
  37. }
  38. // Merge |options| with the |embedder|'s window's options.
  39. const mergeBrowserWindowOptions = function (embedder, options) {
  40. if (options.webPreferences == null) {
  41. options.webPreferences = {}
  42. }
  43. if (embedder.browserWindowOptions != null) {
  44. let parentOptions = embedder.browserWindowOptions
  45. // if parent's visibility is available, that overrides 'show' flag (#12125)
  46. const win = BrowserWindow.fromWebContents(embedder.webContents)
  47. if (win != null) {
  48. parentOptions = { ...embedder.browserWindowOptions, show: win.isVisible() }
  49. }
  50. // Inherit the original options if it is a BrowserWindow.
  51. mergeOptions(options, parentOptions)
  52. } else {
  53. // Or only inherit webPreferences if it is a webview.
  54. mergeOptions(options.webPreferences, embedder.getLastWebPreferences())
  55. }
  56. // Inherit certain option values from parent window
  57. const webPreferences = embedder.getLastWebPreferences()
  58. for (const [name, value] of inheritedWebPreferences) {
  59. if (webPreferences[name] === value) {
  60. options.webPreferences[name] = value
  61. }
  62. }
  63. // Sets correct openerId here to give correct options to 'new-window' event handler
  64. options.webPreferences.openerId = embedder.id
  65. return options
  66. }
  67. // Setup a new guest with |embedder|
  68. const setupGuest = function (embedder, frameName, guest, options) {
  69. // When |embedder| is destroyed we should also destroy attached guest, and if
  70. // guest is closed by user then we should prevent |embedder| from double
  71. // closing guest.
  72. const guestId = guest.webContents.id
  73. const closedByEmbedder = function () {
  74. guest.removeListener('closed', closedByUser)
  75. guest.destroy()
  76. }
  77. const closedByUser = function () {
  78. embedder._sendInternal('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + guestId)
  79. embedder.removeListener('render-view-deleted', closedByEmbedder)
  80. }
  81. embedder.once('render-view-deleted', closedByEmbedder)
  82. guest.once('closed', closedByUser)
  83. if (frameName) {
  84. frameToGuest.set(frameName, guest)
  85. guest.frameName = frameName
  86. guest.once('closed', function () {
  87. frameToGuest.delete(frameName)
  88. })
  89. }
  90. return guestId
  91. }
  92. // Create a new guest created by |embedder| with |options|.
  93. const createGuest = function (embedder, url, referrer, frameName, options, postData) {
  94. let guest = frameToGuest.get(frameName)
  95. if (frameName && (guest != null)) {
  96. guest.loadURL(url)
  97. return guest.webContents.id
  98. }
  99. // Remember the embedder window's id.
  100. if (options.webPreferences == null) {
  101. options.webPreferences = {}
  102. }
  103. guest = new BrowserWindow(options)
  104. if (!options.webContents || url !== 'about:blank') {
  105. // We should not call `loadURL` if the window was constructed from an
  106. // existing webContents(window.open in a sandboxed renderer) and if the url
  107. // is not 'about:blank'.
  108. //
  109. // Navigating to the url when creating the window from an existing
  110. // webContents would not be necessary(it will navigate there anyway), but
  111. // apparently there's a bug that allows the child window to be scripted by
  112. // the opener, even when the child window is from another origin.
  113. //
  114. // That's why the second condition(url !== "about:blank") is required: to
  115. // force `OverrideSiteInstanceForNavigation` to be called and consequently
  116. // spawn a new renderer if the new window is targeting a different origin.
  117. //
  118. // If the URL is "about:blank", then it is very likely that the opener just
  119. // wants to synchronously script the popup, for example:
  120. //
  121. // let popup = window.open()
  122. // popup.document.body.write('<h1>hello</h1>')
  123. //
  124. // The above code would not work if a navigation to "about:blank" is done
  125. // here, since the window would be cleared of all changes in the next tick.
  126. const loadOptions = {
  127. httpReferrer: referrer
  128. }
  129. if (postData != null) {
  130. loadOptions.postData = postData
  131. loadOptions.extraHeaders = 'content-type: application/x-www-form-urlencoded'
  132. if (postData.length > 0) {
  133. const postDataFront = postData[0].bytes.toString()
  134. const boundary = /^--.*[^-\r\n]/.exec(postDataFront)
  135. if (boundary != null) {
  136. loadOptions.extraHeaders = `content-type: multipart/form-data; boundary=${boundary[0].substr(2)}`
  137. }
  138. }
  139. }
  140. guest.loadURL(url, loadOptions)
  141. }
  142. return setupGuest(embedder, frameName, guest, options)
  143. }
  144. const getGuestWindow = function (guestContents) {
  145. let guestWindow = BrowserWindow.fromWebContents(guestContents)
  146. if (guestWindow == null) {
  147. const hostContents = guestContents.hostWebContents
  148. if (hostContents != null) {
  149. guestWindow = BrowserWindow.fromWebContents(hostContents)
  150. }
  151. }
  152. return guestWindow
  153. }
  154. // Checks whether |sender| can access the |target|:
  155. // 1. Check whether |sender| is the parent of |target|.
  156. // 2. Check whether |sender| has node integration, if so it is allowed to
  157. // do anything it wants.
  158. // 3. Check whether the origins match.
  159. //
  160. // However it allows a child window without node integration but with same
  161. // origin to do anything it wants, when its opener window has node integration.
  162. // The W3C does not have anything on this, but from my understanding of the
  163. // security model of |window.opener|, this should be fine.
  164. const canAccessWindow = function (sender, target) {
  165. return (target.getLastWebPreferences().openerId === sender.id) ||
  166. (sender.getLastWebPreferences().nodeIntegration === true) ||
  167. isSameOrigin(sender.getURL(), target.getURL())
  168. }
  169. // Routed window.open messages with raw options
  170. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, features) => {
  171. if (url == null || url === '') url = 'about:blank'
  172. if (frameName == null) frameName = ''
  173. if (features == null) features = ''
  174. const options = {}
  175. const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
  176. const webPreferences = ['zoomFactor', 'nodeIntegration', 'enableRemoteModule', 'preload', 'javascript', 'contextIsolation', 'webviewTag']
  177. const disposition = 'new-window'
  178. // Used to store additional features
  179. const additionalFeatures = []
  180. // Parse the features
  181. parseFeaturesString(features, function (key, value) {
  182. if (value === undefined) {
  183. additionalFeatures.push(key)
  184. } else {
  185. // Don't allow webPreferences to be set since it must be an object
  186. // that cannot be directly overridden
  187. if (key === 'webPreferences') return
  188. if (webPreferences.includes(key)) {
  189. if (options.webPreferences == null) {
  190. options.webPreferences = {}
  191. }
  192. options.webPreferences[key] = value
  193. } else {
  194. options[key] = value
  195. }
  196. }
  197. })
  198. if (options.left) {
  199. if (options.x == null) {
  200. options.x = options.left
  201. }
  202. }
  203. if (options.top) {
  204. if (options.y == null) {
  205. options.y = options.top
  206. }
  207. }
  208. if (options.title == null) {
  209. options.title = frameName
  210. }
  211. if (options.width == null) {
  212. options.width = 800
  213. }
  214. if (options.height == null) {
  215. options.height = 600
  216. }
  217. for (const name of ints) {
  218. if (options[name] != null) {
  219. options[name] = parseInt(options[name], 10)
  220. }
  221. }
  222. const referrer = { url: '', policy: 'default' }
  223. ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event,
  224. url, referrer, frameName, disposition, options, additionalFeatures)
  225. })
  226. // Routed window.open messages with fully parsed options
  227. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', function (event, url, referrer,
  228. frameName, disposition, options,
  229. additionalFeatures, postData) {
  230. options = mergeBrowserWindowOptions(event.sender, options)
  231. event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures, referrer)
  232. const { newGuest } = event
  233. if ((event.sender.isGuest() && !event.sender.allowPopups) || event.defaultPrevented) {
  234. if (newGuest != null) {
  235. if (options.webContents === newGuest.webContents) {
  236. // the webContents is not changed, so set defaultPrevented to false to
  237. // stop the callers of this event from destroying the webContents.
  238. event.defaultPrevented = false
  239. }
  240. event.returnValue = setupGuest(event.sender, frameName, newGuest, options)
  241. } else {
  242. event.returnValue = null
  243. }
  244. } else {
  245. event.returnValue = createGuest(event.sender, url, referrer, frameName, options, postData)
  246. }
  247. })
  248. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', function (event, guestId) {
  249. const guestContents = webContents.fromId(guestId)
  250. if (guestContents == null) return
  251. if (!canAccessWindow(event.sender, guestContents)) {
  252. console.error(`Blocked ${event.sender.getURL()} from closing its opener.`)
  253. return
  254. }
  255. const guestWindow = getGuestWindow(guestContents)
  256. if (guestWindow != null) guestWindow.destroy()
  257. })
  258. const windowMethods = new Set([
  259. 'focus',
  260. 'blur'
  261. ])
  262. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guestId, method, ...args) {
  263. const guestContents = webContents.fromId(guestId)
  264. if (guestContents == null) {
  265. event.returnValue = null
  266. return
  267. }
  268. if (!canAccessWindow(event.sender, guestContents) || !windowMethods.has(method)) {
  269. console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
  270. event.returnValue = null
  271. return
  272. }
  273. const guestWindow = getGuestWindow(guestContents)
  274. if (guestWindow != null) {
  275. event.returnValue = guestWindow[method](...args)
  276. } else {
  277. event.returnValue = null
  278. }
  279. })
  280. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event, guestId, message, targetOrigin, sourceOrigin) {
  281. if (targetOrigin == null) {
  282. targetOrigin = '*'
  283. }
  284. const guestContents = webContents.fromId(guestId)
  285. if (guestContents == null) return
  286. // The W3C does not seem to have word on how postMessage should work when the
  287. // origins do not match, so we do not do |canAccessWindow| check here since
  288. // postMessage across origins is useful and not harmful.
  289. if (targetOrigin === '*' || isSameOrigin(guestContents.getURL(), targetOrigin)) {
  290. const sourceId = event.sender.id
  291. guestContents._sendInternal('ELECTRON_GUEST_WINDOW_POSTMESSAGE', sourceId, message, sourceOrigin)
  292. }
  293. })
  294. const webContentsMethods = new Set([
  295. 'print',
  296. 'executeJavaScript'
  297. ])
  298. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', function (event, guestId, method, ...args) {
  299. const guestContents = webContents.fromId(guestId)
  300. if (guestContents == null) return
  301. if (canAccessWindow(event.sender, guestContents) && webContentsMethods.has(method)) {
  302. guestContents[method](...args)
  303. } else {
  304. console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
  305. }
  306. })
  307. const webContentsSyncMethods = new Set([
  308. 'getURL',
  309. 'loadURL'
  310. ])
  311. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (event, guestId, method, ...args) {
  312. const guestContents = webContents.fromId(guestId)
  313. if (guestContents == null) {
  314. event.returnValue = null
  315. return
  316. }
  317. if (canAccessWindow(event.sender, guestContents) && webContentsSyncMethods.has(method)) {
  318. event.returnValue = guestContents[method](...args)
  319. } else {
  320. console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
  321. event.returnValue = null
  322. }
  323. })