guest-view-manager.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import { webContents } from 'electron/main';
  2. import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
  3. import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils';
  4. import { parseWebViewWebPreferences } from '@electron/internal/browser/parse-features-string';
  5. import { syncMethods, asyncMethods, properties, navigationHistorySyncMethods } from '@electron/internal/common/web-view-methods';
  6. import { webViewEvents } from '@electron/internal/browser/web-view-events';
  7. import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
  8. interface GuestInstance {
  9. elementInstanceId: number;
  10. visibilityState?: DocumentVisibilityState;
  11. embedder: Electron.WebContents;
  12. guest: Electron.WebContents;
  13. }
  14. const webViewManager = process._linkedBinding('electron_browser_web_view_manager');
  15. const netBinding = process._linkedBinding('electron_common_net');
  16. const supportedWebViewEvents = Object.keys(webViewEvents);
  17. const guestInstances = new Map<number, GuestInstance>();
  18. const embedderElementsMap = new Map<string, number>();
  19. function makeWebPreferences (embedder: Electron.WebContents, params: Record<string, any>) {
  20. // parse the 'webpreferences' attribute string, if set
  21. // this uses the same parsing rules as window.open uses for its features
  22. const parsedWebPreferences =
  23. typeof params.webpreferences === 'string'
  24. ? parseWebViewWebPreferences(params.webpreferences)
  25. : null;
  26. const webPreferences: Electron.WebPreferences = {
  27. nodeIntegration: params.nodeintegration ?? false,
  28. nodeIntegrationInSubFrames: params.nodeintegrationinsubframes ?? false,
  29. plugins: params.plugins,
  30. zoomFactor: embedder.zoomFactor,
  31. disablePopups: !params.allowpopups,
  32. webSecurity: !params.disablewebsecurity,
  33. enableBlinkFeatures: params.blinkfeatures,
  34. disableBlinkFeatures: params.disableblinkfeatures,
  35. partition: params.partition,
  36. ...parsedWebPreferences
  37. };
  38. if (params.preload) {
  39. webPreferences.preload = netBinding.fileURLToFilePath(params.preload);
  40. }
  41. // Security options that guest will always inherit from embedder
  42. const inheritedWebPreferences = new Map([
  43. ['contextIsolation', true],
  44. ['javascript', false],
  45. ['nodeIntegration', false],
  46. ['sandbox', true],
  47. ['nodeIntegrationInSubFrames', false],
  48. ['enableWebSQL', false]
  49. ]);
  50. // Inherit certain option values from embedder
  51. const lastWebPreferences = embedder.getLastWebPreferences()!;
  52. for (const [name, value] of inheritedWebPreferences) {
  53. if (lastWebPreferences[name as keyof Electron.WebPreferences] === value) {
  54. (webPreferences as any)[name] = value;
  55. }
  56. }
  57. return webPreferences;
  58. }
  59. function makeLoadURLOptions (params: Record<string, any>) {
  60. const opts: Electron.LoadURLOptions = {};
  61. if (params.httpreferrer) {
  62. opts.httpReferrer = params.httpreferrer;
  63. }
  64. if (params.useragent) {
  65. opts.userAgent = params.useragent;
  66. }
  67. return opts;
  68. }
  69. // Create a new guest instance.
  70. const createGuest = function (embedder: Electron.WebContents, embedderFrameId: number, elementInstanceId: number, params: Record<string, any>) {
  71. const webPreferences = makeWebPreferences(embedder, params);
  72. const event = {
  73. sender: embedder,
  74. preventDefault () {
  75. this.defaultPrevented = true;
  76. },
  77. defaultPrevented: false
  78. };
  79. const { instanceId } = params;
  80. embedder.emit('will-attach-webview', event, webPreferences, params);
  81. if (event.defaultPrevented) {
  82. return -1;
  83. }
  84. const guest = (webContents as typeof ElectronInternal.WebContents).create({
  85. ...webPreferences,
  86. type: 'webview',
  87. embedder
  88. });
  89. const guestInstanceId = guest.id;
  90. guestInstances.set(guestInstanceId, {
  91. elementInstanceId,
  92. guest,
  93. embedder
  94. });
  95. // Clear the guest from map when it is destroyed.
  96. guest.once('destroyed', () => {
  97. if (guestInstances.has(guestInstanceId)) {
  98. detachGuest(embedder, guestInstanceId);
  99. }
  100. });
  101. // Init guest web view after attached.
  102. guest.once('did-attach' as any, function (this: Electron.WebContents, event: Electron.Event) {
  103. const previouslyAttached = this.viewInstanceId != null;
  104. this.viewInstanceId = instanceId;
  105. // Only load URL and set size on first attach
  106. if (previouslyAttached) {
  107. return;
  108. }
  109. if (params.src) {
  110. this.loadURL(params.src, makeLoadURLOptions(params));
  111. }
  112. embedder.emit('did-attach-webview', event, guest);
  113. });
  114. const sendToEmbedder = (channel: string, ...args: any[]) => {
  115. if (!embedder.isDestroyed()) {
  116. embedder._sendInternal(`${channel}-${guest.viewInstanceId}`, ...args);
  117. }
  118. };
  119. const makeProps = (eventKey: string, args: any[]) => {
  120. const props: Record<string, any> = {};
  121. for (const [index, prop] of webViewEvents[eventKey].entries()) {
  122. props[prop] = args[index];
  123. }
  124. return props;
  125. };
  126. // Dispatch events to embedder.
  127. for (const event of supportedWebViewEvents) {
  128. guest.on(event as any, function (_, ...args: any[]) {
  129. sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, event, makeProps(event, args));
  130. });
  131. }
  132. // Dispatch guest's IPC messages to embedder.
  133. guest.on('ipc-message-host' as any, function (event: Electron.IpcMainEvent, channel: string, args: any[]) {
  134. sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, 'ipc-message', {
  135. frameId: [event.processId, event.frameId],
  136. channel,
  137. args
  138. });
  139. });
  140. // Dispatch guest's frame navigation event to embedder.
  141. guest.on('will-frame-navigate', function (event: Electron.WebContentsWillFrameNavigateEventParams) {
  142. sendToEmbedder(IPC_MESSAGES.GUEST_VIEW_INTERNAL_DISPATCH_EVENT, 'will-frame-navigate', {
  143. url: event.url,
  144. isMainFrame: event.isMainFrame,
  145. frameProcessId: event.frame.processId,
  146. frameRoutingId: event.frame.routingId
  147. });
  148. });
  149. // Notify guest of embedder window visibility when it is ready
  150. // FIXME Remove once https://github.com/electron/electron/issues/6828 is fixed
  151. guest.on('dom-ready', function () {
  152. const guestInstance = guestInstances.get(guestInstanceId);
  153. if (guestInstance != null && guestInstance.visibilityState != null) {
  154. guest._sendInternal(IPC_MESSAGES.GUEST_INSTANCE_VISIBILITY_CHANGE, guestInstance.visibilityState);
  155. }
  156. });
  157. // Destroy the old guest when attaching.
  158. const key = `${embedder.id}-${elementInstanceId}`;
  159. const oldGuestInstanceId = embedderElementsMap.get(key);
  160. if (oldGuestInstanceId != null) {
  161. const oldGuestInstance = guestInstances.get(oldGuestInstanceId);
  162. if (oldGuestInstance) {
  163. oldGuestInstance.guest.detachFromOuterFrame();
  164. }
  165. }
  166. embedderElementsMap.set(key, guestInstanceId);
  167. guest.setEmbedder(embedder);
  168. watchEmbedder(embedder);
  169. webViewManager.addGuest(guestInstanceId, embedder, guest, webPreferences);
  170. guest.attachToIframe(embedder, embedderFrameId);
  171. return guestInstanceId;
  172. };
  173. // Remove an guest-embedder relationship.
  174. const detachGuest = function (embedder: Electron.WebContents, guestInstanceId: number) {
  175. const guestInstance = guestInstances.get(guestInstanceId);
  176. if (!guestInstance) return;
  177. if (embedder !== guestInstance.embedder) {
  178. return;
  179. }
  180. webViewManager.removeGuest(embedder, guestInstanceId);
  181. guestInstances.delete(guestInstanceId);
  182. const key = `${embedder.id}-${guestInstance.elementInstanceId}`;
  183. embedderElementsMap.delete(key);
  184. };
  185. // Once an embedder has had a guest attached we watch it for destruction to
  186. // destroy any remaining guests.
  187. const watchedEmbedders = new Set<Electron.WebContents>();
  188. const watchEmbedder = function (embedder: Electron.WebContents) {
  189. if (watchedEmbedders.has(embedder)) {
  190. return;
  191. }
  192. watchedEmbedders.add(embedder);
  193. // Forward embedder window visibility change events to guest
  194. const onVisibilityChange = function (visibilityState: DocumentVisibilityState) {
  195. for (const guestInstance of guestInstances.values()) {
  196. guestInstance.visibilityState = visibilityState;
  197. if (guestInstance.embedder === embedder) {
  198. guestInstance.guest._sendInternal(IPC_MESSAGES.GUEST_INSTANCE_VISIBILITY_CHANGE, visibilityState);
  199. }
  200. }
  201. };
  202. embedder.on('-window-visibility-change' as any, onVisibilityChange);
  203. embedder.once('will-destroy' as any, () => {
  204. // Usually the guestInstances is cleared when guest is destroyed, but it
  205. // may happen that the embedder gets manually destroyed earlier than guest,
  206. // and the embedder will be invalid in the usual code path.
  207. for (const [guestInstanceId, guestInstance] of guestInstances) {
  208. if (guestInstance.embedder === embedder) {
  209. detachGuest(embedder, guestInstanceId);
  210. }
  211. }
  212. // Clear the listeners.
  213. embedder.removeListener('-window-visibility-change' as any, onVisibilityChange);
  214. watchedEmbedders.delete(embedder);
  215. });
  216. };
  217. const isWebViewTagEnabledCache = new WeakMap();
  218. const isWebViewTagEnabled = function (contents: Electron.WebContents) {
  219. if (!isWebViewTagEnabledCache.has(contents)) {
  220. const webPreferences = contents.getLastWebPreferences() || {};
  221. isWebViewTagEnabledCache.set(contents, !!webPreferences.webviewTag);
  222. }
  223. return isWebViewTagEnabledCache.get(contents);
  224. };
  225. const makeSafeHandler = function<Event extends { sender: Electron.WebContents }> (channel: string, handler: (event: Event, ...args: any[]) => any) {
  226. return (event: Event, ...args: any[]) => {
  227. if (isWebViewTagEnabled(event.sender)) {
  228. return handler(event, ...args);
  229. } else {
  230. console.error(`<webview> IPC message ${channel} sent by WebContents with <webview> disabled (${event.sender.id})`);
  231. throw new Error('<webview> disabled');
  232. }
  233. };
  234. };
  235. const handleMessage = function (channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
  236. ipcMainInternal.handle(channel, makeSafeHandler(channel, handler));
  237. };
  238. const handleMessageSync = function (channel: string, handler: (event: ElectronInternal.IpcMainInternalEvent, ...args: any[]) => any) {
  239. ipcMainUtils.handleSync(channel, makeSafeHandler(channel, handler));
  240. };
  241. handleMessage(IPC_MESSAGES.GUEST_VIEW_MANAGER_CREATE_AND_ATTACH_GUEST, function (event, embedderFrameId: number, elementInstanceId: number, params) {
  242. return createGuest(event.sender, embedderFrameId, elementInstanceId, params);
  243. });
  244. handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_DETACH_GUEST, function (event, guestInstanceId: number) {
  245. return detachGuest(event.sender, guestInstanceId);
  246. });
  247. // this message is sent by the actual <webview>
  248. ipcMainInternal.on(IPC_MESSAGES.GUEST_VIEW_MANAGER_FOCUS_CHANGE, function (event: ElectronInternal.IpcMainInternalEvent, focus: boolean) {
  249. event.sender.emit('-focus-change', {}, focus);
  250. });
  251. handleMessage(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, function (event, guestInstanceId: number, method: string, args: any[]) {
  252. const guest = getGuestForWebContents(guestInstanceId, event.sender);
  253. if (!asyncMethods.has(method)) {
  254. throw new Error(`Invalid method: ${method}`);
  255. }
  256. return (guest as any)[method](...args);
  257. });
  258. handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_CALL, function (event, guestInstanceId: number, method: string, args: any[]) {
  259. const guest = getGuestForWebContents(guestInstanceId, event.sender);
  260. if (!syncMethods.has(method)) {
  261. throw new Error(`Invalid method: ${method}`);
  262. }
  263. // Redirect history methods to updated navigationHistory property on webContents. See issue #42879.
  264. if (navigationHistorySyncMethods.has(method)) {
  265. let navigationMethod = method;
  266. if (method === 'clearHistory') {
  267. navigationMethod = 'clear';
  268. }
  269. return (guest as any).navigationHistory[navigationMethod](...args);
  270. }
  271. return (guest as any)[method](...args);
  272. });
  273. handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_GET, function (event, guestInstanceId: number, property: string) {
  274. const guest = getGuestForWebContents(guestInstanceId, event.sender);
  275. if (!properties.has(property)) {
  276. throw new Error(`Invalid property: ${property}`);
  277. }
  278. return (guest as any)[property];
  279. });
  280. handleMessageSync(IPC_MESSAGES.GUEST_VIEW_MANAGER_PROPERTY_SET, function (event, guestInstanceId: number, property: string, val: any) {
  281. const guest = getGuestForWebContents(guestInstanceId, event.sender);
  282. if (!properties.has(property)) {
  283. throw new Error(`Invalid property: ${property}`);
  284. }
  285. (guest as any)[property] = val;
  286. });
  287. // Returns WebContents from its guest id hosted in given webContents.
  288. const getGuestForWebContents = function (guestInstanceId: number, contents: Electron.WebContents) {
  289. const guestInstance = guestInstances.get(guestInstanceId);
  290. if (!guestInstance) {
  291. throw new Error(`Invalid guestInstanceId: ${guestInstanceId}`);
  292. }
  293. if (guestInstance.guest.hostWebContents !== contents) {
  294. throw new Error(`Access denied to guestInstanceId: ${guestInstanceId}`);
  295. }
  296. return guestInstance.guest;
  297. };