guest-window-manager.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. 'use strict'
  2. const {BrowserWindow, ipcMain, webContents} = require('electron')
  3. const {isSameOrigin} = process.atomBinding('v8_util')
  4. const hasProp = {}.hasOwnProperty
  5. const frameToGuest = {}
  6. // Copy attribute of |parent| to |child| if it is not defined in |child|.
  7. const mergeOptions = function (child, parent) {
  8. let key, value
  9. for (key in parent) {
  10. if (!hasProp.call(parent, key)) continue
  11. value = parent[key]
  12. if (!(key in child)) {
  13. if (typeof value === 'object') {
  14. child[key] = mergeOptions({}, value)
  15. } else {
  16. child[key] = value
  17. }
  18. }
  19. }
  20. return child
  21. }
  22. // Merge |options| with the |embedder|'s window's options.
  23. const mergeBrowserWindowOptions = function (embedder, options) {
  24. if (embedder.browserWindowOptions != null) {
  25. // Inherit the original options if it is a BrowserWindow.
  26. mergeOptions(options, embedder.browserWindowOptions)
  27. } else {
  28. // Or only inherit web-preferences if it is a webview.
  29. if (options.webPreferences == null) {
  30. options.webPreferences = {}
  31. }
  32. mergeOptions(options.webPreferences, embedder.getWebPreferences())
  33. }
  34. // Disable node integration on child window if disabled on parent window
  35. if (embedder.getWebPreferences().nodeIntegration === false) {
  36. options.webPreferences.nodeIntegration = false
  37. }
  38. return options
  39. }
  40. // Create a new guest created by |embedder| with |options|.
  41. const createGuest = function (embedder, url, frameName, options) {
  42. let guest = frameToGuest[frameName]
  43. if (frameName && (guest != null)) {
  44. guest.loadURL(url)
  45. return guest.id
  46. }
  47. // Remember the embedder window's id.
  48. if (options.webPreferences == null) {
  49. options.webPreferences = {}
  50. }
  51. options.webPreferences.openerId = embedder.id
  52. guest = new BrowserWindow(options)
  53. guest.loadURL(url)
  54. // When |embedder| is destroyed we should also destroy attached guest, and if
  55. // guest is closed by user then we should prevent |embedder| from double
  56. // closing guest.
  57. const guestId = guest.webContents.id
  58. const closedByEmbedder = function () {
  59. guest.removeListener('closed', closedByUser)
  60. guest.destroy()
  61. }
  62. const closedByUser = function () {
  63. embedder.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + guestId)
  64. embedder.removeListener('render-view-deleted', closedByEmbedder)
  65. }
  66. embedder.once('render-view-deleted', closedByEmbedder)
  67. guest.once('closed', closedByUser)
  68. if (frameName) {
  69. frameToGuest[frameName] = guest
  70. guest.frameName = frameName
  71. guest.once('closed', function () {
  72. delete frameToGuest[frameName]
  73. })
  74. }
  75. return guestId
  76. }
  77. const getGuestWindow = function (guestContents) {
  78. let guestWindow = BrowserWindow.fromWebContents(guestContents)
  79. if (guestWindow == null) {
  80. const hostContents = guestContents.hostWebContents
  81. if (hostContents != null) {
  82. guestWindow = BrowserWindow.fromWebContents(hostContents)
  83. }
  84. }
  85. return guestWindow
  86. }
  87. // Checks whether |sender| can access the |target|:
  88. // 1. Check whether |sender| is the parent of |target|.
  89. // 2. Check whether |sender| has node integration, if so it is allowed to
  90. // do anything it wants.
  91. // 3. Check whether the origins match.
  92. //
  93. // However it allows a child window without node integration but with same
  94. // origin to do anything it wants, when its opener window has node integration.
  95. // The W3C does not have anything on this, but from my understanding of the
  96. // security model of |window.opener|, this should be fine.
  97. const canAccessWindow = function (sender, target) {
  98. return (target.getWebPreferences().openerId === sender.id) ||
  99. (sender.getWebPreferences().nodeIntegration === true) ||
  100. isSameOrigin(sender.getURL(), target.getURL())
  101. }
  102. // Routed window.open messages.
  103. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, frameName, disposition, options) {
  104. options = mergeBrowserWindowOptions(event.sender, options)
  105. event.sender.emit('new-window', event, url, frameName, disposition, options)
  106. if ((event.sender.isGuest() && !event.sender.allowPopups) || event.defaultPrevented) {
  107. event.returnValue = null
  108. } else {
  109. event.returnValue = createGuest(event.sender, url, frameName, options)
  110. }
  111. })
  112. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', function (event, guestId) {
  113. const guestContents = webContents.fromId(guestId)
  114. if (guestContents == null) return
  115. if (!canAccessWindow(event.sender, guestContents)) {
  116. console.error(`Blocked ${event.sender.getURL()} from closing its opener.`)
  117. return
  118. }
  119. const guestWindow = getGuestWindow(guestContents)
  120. if (guestWindow != null) guestWindow.destroy()
  121. })
  122. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guestId, method, ...args) {
  123. const guestContents = webContents.fromId(guestId)
  124. if (guestContents == null) {
  125. event.returnValue = null
  126. return
  127. }
  128. if (!canAccessWindow(event.sender, guestContents)) {
  129. console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
  130. event.returnValue = null
  131. return
  132. }
  133. const guestWindow = getGuestWindow(guestContents)
  134. if (guestWindow != null) {
  135. event.returnValue = guestWindow[method](...args)
  136. } else {
  137. event.returnValue = null
  138. }
  139. })
  140. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event, guestId, message, targetOrigin, sourceOrigin) {
  141. const guestContents = webContents.fromId(guestId)
  142. if (guestContents == null) return
  143. // The W3C does not seem to have word on how postMessage should work when the
  144. // origins do not match, so we do not do |canAccessWindow| check here since
  145. // postMessage across origins is useful and not harmful.
  146. if (guestContents.getURL().indexOf(targetOrigin) === 0 || targetOrigin === '*') {
  147. const sourceId = event.sender.id
  148. guestContents.send('ELECTRON_GUEST_WINDOW_POSTMESSAGE', sourceId, message, sourceOrigin)
  149. }
  150. })
  151. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', function (event, guestId, method, ...args) {
  152. const guestContents = webContents.fromId(guestId)
  153. if (guestContents == null) return
  154. if (canAccessWindow(event.sender, guestContents)) {
  155. guestContents[method](...args)
  156. } else {
  157. console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
  158. }
  159. })
  160. ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (event, guestId, method, ...args) {
  161. const guestContents = webContents.fromId(guestId)
  162. if (guestContents == null) {
  163. event.returnValue = null
  164. return
  165. }
  166. if (canAccessWindow(event.sender, guestContents)) {
  167. event.returnValue = guestContents[method](...args)
  168. } else {
  169. console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`)
  170. event.returnValue = null
  171. }
  172. })