guest-window-manager.js 12 KB

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