diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index d3f5b82a773fd..71f223081bdd6 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -862,6 +862,8 @@ Returns: Emitted when the renderer process sends an asynchronous message via `ipcRenderer.send()`. +See also [`webContents.ipc`](#contentsipc-readonly), which provides an [`IpcMain`](ipc-main.md)-like interface for responding to IPC messages specifically from this WebContents. + #### Event: 'ipc-message-sync' Returns: @@ -872,6 +874,8 @@ Returns: Emitted when the renderer process sends a synchronous message via `ipcRenderer.sendSync()`. +See also [`webContents.ipc`](#contentsipc-readonly), which provides an [`IpcMain`](ipc-main.md)-like interface for responding to IPC messages specifically from this WebContents. + #### Event: 'preferred-size-changed' Returns: @@ -1985,6 +1989,35 @@ This corresponds to the [animationPolicy][] accessibility feature in Chromium. ### Instance Properties +#### `contents.ipc` _Readonly_ + +An [`IpcMain`](ipc-main.md) scoped to just IPC messages sent from this +WebContents. + +IPC messages sent with `ipcRenderer.send`, `ipcRenderer.sendSync` or +`ipcRenderer.postMessage` will be delivered in the following order: + +1. `contents.on('ipc-message')` +2. `contents.mainFrame.on(channel)` +3. `contents.ipc.on(channel)` +4. `ipcMain.on(channel)` + +Handlers registered with `invoke` will be checked in the following order. The +first one that is defined will be called, the rest will be ignored. + +1. `contents.mainFrame.handle(channel)` +2. `contents.handle(channel)` +3. `ipcMain.handle(channel)` + +A handler or event listener registered on the WebContents will receive IPC +messages sent from any frame, including child frames. In most cases, only the +main frame can send IPC messages. However, if the `nodeIntegrationInSubFrames` +option is enabled, it is possible for child frames to send IPC messages also. +In that case, handlers should check the `senderFrame` property of the IPC event +to ensure that the message is coming from the expected frame. Alternatively, +register handlers on the appropriate frame directly using the +[`WebFrameMain.ipc`](web-frame-main.md#frameipc-readonly) interface. + #### `contents.audioMuted` A `boolean` property that determines whether this page is muted. diff --git a/docs/api/web-frame-main.md b/docs/api/web-frame-main.md index e97041668e15b..8ce004b6e9172 100644 --- a/docs/api/web-frame-main.md +++ b/docs/api/web-frame-main.md @@ -140,6 +140,31 @@ ipcRenderer.on('port', (e, msg) => { ### Instance Properties +#### `frame.ipc` _Readonly_ + +An [`IpcMain`](ipc-main.md) instance scoped to the frame. + +IPC messages sent with `ipcRenderer.send`, `ipcRenderer.sendSync` or +`ipcRenderer.postMessage` will be delivered in the following order: + +1. `contents.on('ipc-message')` +2. `contents.mainFrame.on(channel)` +3. `contents.ipc.on(channel)` +4. `ipcMain.on(channel)` + +Handlers registered with `invoke` will be checked in the following order. The +first one that is defined will be called, the rest will be ignored. + +1. `contents.mainFrame.handle(channel)` +2. `contents.handle(channel)` +3. `ipcMain.handle(channel)` + +In most cases, only the main frame of a WebContents can send or receive IPC +messages. However, if the `nodeIntegrationInSubFrames` option is enabled, it is +possible for child frames to send and receive IPC messages also. The +[`WebContents.ipc`](web-contents.md#contentsipc-readonly) interface may be more +convenient when `nodeIntegrationInSubFrames` is not enabled. + #### `frame.url` _Readonly_ A `string` representing the current URL of the frame. diff --git a/lib/browser/api/ipc-main.ts b/lib/browser/api/ipc-main.ts index 20c7fb9dc5cf6..40aa4efd90aeb 100644 --- a/lib/browser/api/ipc-main.ts +++ b/lib/browser/api/ipc-main.ts @@ -2,7 +2,4 @@ import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl'; const ipcMain = new IpcMainImpl(); -// Do not throw exception when channel name is "error". -ipcMain.on('error', () => {}); - export default ipcMain; diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts index 6433bed4df568..61378c00dee37 100644 --- a/lib/browser/api/web-contents.ts +++ b/lib/browser/api/web-contents.ts @@ -9,12 +9,15 @@ import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal'; import * as ipcMainUtils from '@electron/internal/browser/ipc-main-internal-utils'; import { MessagePortMain } from '@electron/internal/browser/message-port-main'; import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages'; +import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl'; // session is not used here, the purpose is to make sure session is initialized // before the webContents module. // eslint-disable-next-line session +const webFrameMainBinding = process._linkedBinding('electron_browser_web_frame_main'); + let nextId = 0; const getNextId = function () { return ++nextId; @@ -556,6 +559,12 @@ WebContents.prototype._init = function () { this._windowOpenHandler = null; + const ipc = new IpcMainImpl(); + Object.defineProperty(this, 'ipc', { + get () { return ipc; }, + enumerable: true + }); + // Dispatch IPC messages to the ipc module. this.on('-ipc-message' as any, function (this: Electron.WebContents, event: Electron.IpcMainEvent, internal: boolean, channel: string, args: any[]) { addSenderFrameToEvent(event); @@ -564,6 +573,9 @@ WebContents.prototype._init = function () { } else { addReplyToEvent(event); this.emit('ipc-message', event, channel, ...args); + const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId); + maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args); + ipc.emit(channel, event, ...args); ipcMain.emit(channel, event, ...args); } }); @@ -575,8 +587,10 @@ WebContents.prototype._init = function () { console.error(`Error occurred in handler for '${channel}':`, error); event.sendReply({ error: error.toString() }); }; - const target = internal ? ipcMainInternal : ipcMain; - if ((target as any)._invokeHandlers.has(channel)) { + const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId); + const targets: (ElectronInternal.IpcMainInternal| undefined)[] = internal ? [ipcMainInternal] : [maybeWebFrame && maybeWebFrame.ipc, ipc, ipcMain]; + const target = targets.find(target => target && (target as any)._invokeHandlers.has(channel)); + if (target) { (target as any)._invokeHandlers.get(channel)(event, ...args); } else { event._throw(`No handler registered for '${channel}'`); @@ -590,10 +604,13 @@ WebContents.prototype._init = function () { ipcMainInternal.emit(channel, event, ...args); } else { addReplyToEvent(event); - if (this.listenerCount('ipc-message-sync') === 0 && ipcMain.listenerCount(channel) === 0) { + const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId); + if (this.listenerCount('ipc-message-sync') === 0 && ipc.listenerCount(channel) === 0 && ipcMain.listenerCount(channel) === 0 && (!maybeWebFrame || maybeWebFrame.ipc.listenerCount(channel) === 0)) { console.warn(`WebContents #${this.id} called ipcRenderer.sendSync() with '${channel}' channel without listeners.`); } this.emit('ipc-message-sync', event, channel, ...args); + maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, ...args); + ipc.emit(channel, event, ...args); ipcMain.emit(channel, event, ...args); } }); @@ -601,6 +618,9 @@ WebContents.prototype._init = function () { this.on('-ipc-ports' as any, function (event: Electron.IpcMainEvent, internal: boolean, channel: string, message: any, ports: any[]) { addSenderFrameToEvent(event); event.ports = ports.map(p => new MessagePortMain(p)); + ipc.emit(channel, event, message); + const maybeWebFrame = webFrameMainBinding.fromIdOrNull(event.processId, event.frameId); + maybeWebFrame && maybeWebFrame.ipc.emit(channel, event, message); ipcMain.emit(channel, event, message); }); diff --git a/lib/browser/api/web-frame-main.ts b/lib/browser/api/web-frame-main.ts index 2f75ee615a697..95e4a2fa4d636 100644 --- a/lib/browser/api/web-frame-main.ts +++ b/lib/browser/api/web-frame-main.ts @@ -1,7 +1,16 @@ import { MessagePortMain } from '@electron/internal/browser/message-port-main'; +import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl'; const { WebFrameMain, fromId } = process._linkedBinding('electron_browser_web_frame_main'); +Object.defineProperty(WebFrameMain.prototype, 'ipc', { + get () { + const ipc = new IpcMainImpl(); + Object.defineProperty(this, 'ipc', { value: ipc }); + return ipc; + } +}); + WebFrameMain.prototype.send = function (channel, ...args) { if (typeof channel !== 'string') { throw new Error('Missing required channel argument'); diff --git a/lib/browser/ipc-main-impl.ts b/lib/browser/ipc-main-impl.ts index 9118f03e19498..0349cc00c0b4e 100644 --- a/lib/browser/ipc-main-impl.ts +++ b/lib/browser/ipc-main-impl.ts @@ -4,6 +4,13 @@ import { IpcMainInvokeEvent } from 'electron/main'; export class IpcMainImpl extends EventEmitter { private _invokeHandlers: Map void> = new Map(); + constructor () { + super(); + + // Do not throw exception when channel name is "error". + this.on('error', () => {}); + } + handle: Electron.IpcMain['handle'] = (method, fn) => { if (this._invokeHandlers.has(method)) { throw new Error(`Attempted to register a second handler for '${method}'`); diff --git a/lib/browser/ipc-main-internal.ts b/lib/browser/ipc-main-internal.ts index f6c24537d324c..0925f1ef87485 100644 --- a/lib/browser/ipc-main-internal.ts +++ b/lib/browser/ipc-main-internal.ts @@ -1,6 +1,3 @@ import { IpcMainImpl } from '@electron/internal/browser/ipc-main-impl'; export const ipcMainInternal = new IpcMainImpl() as ElectronInternal.IpcMainInternal; - -// Do not throw exception when channel name is "error". -ipcMainInternal.on('error', () => {}); diff --git a/shell/browser/api/electron_api_web_frame_main.cc b/shell/browser/api/electron_api_web_frame_main.cc index 1c98ed6376461..f4bbe118ba248 100644 --- a/shell/browser/api/electron_api_web_frame_main.cc +++ b/shell/browser/api/electron_api_web_frame_main.cc @@ -362,6 +362,18 @@ gin::Handle WebFrameMain::From(v8::Isolate* isolate, return handle; } +// static +gin::Handle WebFrameMain::FromOrNull( + v8::Isolate* isolate, + content::RenderFrameHost* rfh) { + if (rfh == nullptr) + return gin::Handle(); + auto* web_frame = FromRenderFrameHost(rfh); + if (web_frame) + return gin::CreateHandle(isolate, web_frame); + return gin::Handle(); +} + // static v8::Local WebFrameMain::FillObjectTemplate( v8::Isolate* isolate, @@ -409,6 +421,20 @@ v8::Local FromID(gin_helper::ErrorThrower thrower, return WebFrameMain::From(thrower.isolate(), rfh).ToV8(); } +v8::Local FromIDOrNull(gin_helper::ErrorThrower thrower, + int render_process_id, + int render_frame_id) { + if (!electron::Browser::Get()->is_ready()) { + thrower.ThrowError("WebFrameMain is available only after app ready"); + return v8::Null(thrower.isolate()); + } + + auto* rfh = + content::RenderFrameHost::FromID(render_process_id, render_frame_id); + + return WebFrameMain::FromOrNull(thrower.isolate(), rfh).ToV8(); +} + void Initialize(v8::Local exports, v8::Local unused, v8::Local context, @@ -417,6 +443,7 @@ void Initialize(v8::Local exports, gin_helper::Dictionary dict(isolate, exports); dict.Set("WebFrameMain", WebFrameMain::GetConstructor(context)); dict.SetMethod("fromId", &FromID); + dict.SetMethod("fromIdOrNull", &FromIDOrNull); } } // namespace diff --git a/shell/browser/api/electron_api_web_frame_main.h b/shell/browser/api/electron_api_web_frame_main.h index eb6138351f330..e8f1715ab83a2 100644 --- a/shell/browser/api/electron_api_web_frame_main.h +++ b/shell/browser/api/electron_api_web_frame_main.h @@ -44,6 +44,9 @@ class WebFrameMain : public gin::Wrappable, static gin::Handle From( v8::Isolate* isolate, content::RenderFrameHost* render_frame_host); + static gin::Handle FromOrNull( + v8::Isolate* isolate, + content::RenderFrameHost* render_frame_host); static WebFrameMain* FromFrameTreeNodeId(int frame_tree_node_id); static WebFrameMain* FromRenderFrameHost( content::RenderFrameHost* render_frame_host); diff --git a/spec-main/api-ipc-spec.ts b/spec-main/api-ipc-spec.ts index 0595531be86ff..c63c9ba94eb85 100644 --- a/spec-main/api-ipc-spec.ts +++ b/spec-main/api-ipc-spec.ts @@ -3,8 +3,13 @@ import { expect } from 'chai'; import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main'; import { closeAllWindows } from './window-helpers'; import { emittedOnce } from './events-helpers'; +import { defer } from './spec-helpers'; +import * as path from 'path'; +import * as http from 'http'; +import { AddressInfo } from 'net'; const v8Util = process._linkedBinding('electron_common_v8_util'); +const fixturesPath = path.resolve(__dirname, 'fixtures'); describe('ipc module', () => { describe('invoke', () => { @@ -90,7 +95,7 @@ describe('ipc module', () => { }); it('throws an error when invoking a handler that was removed', async () => { - ipcMain.handle('test', () => {}); + ipcMain.handle('test', () => { }); ipcMain.removeHandler('test'); const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { expect(arg.error).to.match(/No handler registered/); @@ -101,9 +106,9 @@ describe('ipc module', () => { }); it('forbids multiple handlers', async () => { - ipcMain.handle('test', () => {}); + ipcMain.handle('test', () => { }); try { - expect(() => { ipcMain.handle('test', () => {}); }).to.throw(/second handler/); + expect(() => { ipcMain.handle('test', () => { }); }).to.throw(/second handler/); } finally { ipcMain.removeHandler('test'); } @@ -563,4 +568,195 @@ describe('ipc module', () => { generateTests('WebContents.postMessage', contents => contents.postMessage.bind(contents)); generateTests('WebFrameMain.postMessage', contents => contents.mainFrame.postMessage.bind(contents.mainFrame)); }); + + describe('WebContents.ipc', () => { + afterEach(closeAllWindows); + + it('receives ipc messages sent from the WebContents', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)'); + const [, num] = await emittedOnce(w.webContents.ipc, 'test'); + expect(num).to.equal(42); + }); + + it('receives sync-ipc messages sent from the WebContents', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.ipc.on('test', (event, arg) => { + event.returnValue = arg * 2; + }); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])'); + const [event] = await emittedOnce(w.webContents.ipc, 'test'); + expect(event.ports.length).to.equal(1); + }); + + it('handles invoke messages sent from the WebContents', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.ipc.handle('test', (_event, arg) => arg * 2); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('cascades to ipcMain', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + let gotFromIpcMain = false; + const ipcMainReceived = new Promise(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); })); + const ipcReceived = new Promise(resolve => w.webContents.ipc.on('test', () => { resolve(gotFromIpcMain); })); + defer(() => ipcMain.removeAllListeners('test')); + w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)'); + + // assert that they are delivered in the correct order + expect(await ipcReceived).to.be.false(); + await ipcMainReceived; + }); + + it('overrides ipcMain handlers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.ipc.handle('test', (_event, arg) => arg * 2); + ipcMain.handle('test', () => { throw new Error('should not be called'); }); + defer(() => ipcMain.removeHandler('test')); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('falls back to ipcMain handlers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + ipcMain.handle('test', (_event, arg) => { return arg * 2; }); + defer(() => ipcMain.removeHandler('test')); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('receives ipcs from child frames', async () => { + const server = http.createServer((req, res) => { + res.setHeader('content-type', 'text/html'); + res.end(''); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const port = (server.address() as AddressInfo).port; + defer(() => { + server.close(); + }); + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.resolve(fixturesPath, 'preload-expose-ipc.js') } }); + // Preloads don't run in about:blank windows, and file:// urls can't be loaded in iframes, so use a blank http page. + await w.loadURL(`data:text/html,`); + w.webContents.mainFrame.frames[0].executeJavaScript('ipc.send(\'test\', 42)'); + const [, arg] = await emittedOnce(w.webContents.ipc, 'test'); + expect(arg).to.equal(42); + }); + }); + + describe('WebFrameMain.ipc', () => { + afterEach(closeAllWindows); + it('responds to ipc messages in the main frame', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)'); + const [, arg] = await emittedOnce(w.webContents.mainFrame.ipc, 'test'); + expect(arg).to.equal(42); + }); + + it('responds to sync ipc messages in the main frame', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.mainFrame.ipc.on('test', (event, arg) => { + event.returnValue = arg * 2; + }); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.sendSync(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('receives postMessage messages sent from the WebContents, w/ MessagePorts', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.postMessage(\'test\', null, [(new MessageChannel).port1])'); + const [event] = await emittedOnce(w.webContents.mainFrame.ipc, 'test'); + expect(event.ports.length).to.equal(1); + }); + + it('handles invoke messages sent from the WebContents', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('cascades to WebContents and ipcMain', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + let gotFromIpcMain = false; + let gotFromWebContents = false; + const ipcMainReceived = new Promise(resolve => ipcMain.on('test', () => { gotFromIpcMain = true; resolve(); })); + const ipcWebContentsReceived = new Promise(resolve => w.webContents.ipc.on('test', () => { gotFromWebContents = true; resolve(gotFromIpcMain); })); + const ipcReceived = new Promise(resolve => w.webContents.mainFrame.ipc.on('test', () => { resolve(gotFromWebContents); })); + defer(() => ipcMain.removeAllListeners('test')); + w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.send(\'test\', 42)'); + + // assert that they are delivered in the correct order + expect(await ipcReceived).to.be.false(); + expect(await ipcWebContentsReceived).to.be.false(); + await ipcMainReceived; + }); + + it('overrides ipcMain handlers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2); + ipcMain.handle('test', () => { throw new Error('should not be called'); }); + defer(() => ipcMain.removeHandler('test')); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('overrides WebContents handlers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.ipc.handle('test', () => { throw new Error('should not be called'); }); + w.webContents.mainFrame.ipc.handle('test', (_event, arg) => arg * 2); + ipcMain.handle('test', () => { throw new Error('should not be called'); }); + defer(() => ipcMain.removeHandler('test')); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('falls back to WebContents handlers', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.loadURL('about:blank'); + w.webContents.ipc.handle('test', (_event, arg) => { return arg * 2; }); + const result = await w.webContents.executeJavaScript('require(\'electron\').ipcRenderer.invoke(\'test\', 42)'); + expect(result).to.equal(42 * 2); + }); + + it('receives ipcs from child frames', async () => { + const server = http.createServer((req, res) => { + res.setHeader('content-type', 'text/html'); + res.end(''); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const port = (server.address() as AddressInfo).port; + defer(() => { + server.close(); + }); + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegrationInSubFrames: true, preload: path.resolve(fixturesPath, 'preload-expose-ipc.js') } }); + // Preloads don't run in about:blank windows, and file:// urls can't be loaded in iframes, so use a blank http page. + await w.loadURL(`data:text/html,`); + w.webContents.mainFrame.frames[0].executeJavaScript('ipc.send(\'test\', 42)'); + w.webContents.mainFrame.ipc.on('test', () => { throw new Error('should not be called'); }); + const [, arg] = await emittedOnce(w.webContents.mainFrame.frames[0].ipc, 'test'); + expect(arg).to.equal(42); + }); + }); }); diff --git a/spec-main/fixtures/preload-expose-ipc.js b/spec-main/fixtures/preload-expose-ipc.js new file mode 100644 index 0000000000000..131399e0b0185 --- /dev/null +++ b/spec-main/fixtures/preload-expose-ipc.js @@ -0,0 +1,14 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +// NOTE: Never do this in an actual app! Very insecure! +contextBridge.exposeInMainWorld('ipc', { + send (...args) { + return ipcRenderer.send(...args); + }, + sendSync (...args) { + return ipcRenderer.sendSync(...args); + }, + invoke (...args) { + return ipcRenderer.invoke(...args); + } +}); diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 950d848f50cb2..7fcd2ecb2acc2 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -237,6 +237,7 @@ declare namespace NodeJS { _linkedBinding(name: 'electron_browser_web_frame_main'): { WebFrameMain: typeof Electron.WebFrameMain; fromId(processId: number, routingId: number): Electron.WebFrameMain; + fromIdOrNull(processId: number, routingId: number): Electron.WebFrameMain; } _linkedBinding(name: 'electron_renderer_crash_reporter'): Electron.CrashReporter; _linkedBinding(name: 'electron_renderer_ipc'): { ipc: IpcRendererBinding };