guest-window-manager.js 11 KB

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