guest-view-manager.ts 14 KB

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