guest-window-manager.js 12 KB

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