guest-window-manager.js 12 KB

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