guest-window-manager.js 12 KB

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