Electron: Communicate between BrowserWindow and rendered URL (nodeIntegration: false) - javascript

I've spent about an hour reading gist after repo after blog post, but can't seem to figure out how to do do this.
I have a BrowserWindow instance loading a URL (that I control), with nodeIntegration: false.
From the main process, I'd like to communicate with the rendered URL. I'm getting confused between preload scripts, BrowserWindow.send and executeJavascript paradigms.
The data I want to send is very large (eg. file uploads between 50kb and 10mb).
What's the best way to do this? Any any examples/tutorials you may know about would be helpful. Thanks!

main.js
const path = require('path')
const electron = require('electron')
const { app, BrowserWindow, ipcMain } = electron
const window = new BrowserWindow({
minWidth: 1200,
minHeight: 700,
autoHideMenuBar: true,
resizable: true,
show: false,
scrollBounce: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
}
})
window.webContents.loadURL('https://xxx.xxx.com') // load your web page
ipcMain.on('ping', (event, msg) => {
console.log(msg) // msg from web page
window.webContents.send('pong', 'hi web page') // send to web page
})
preload.js
const { ipcRenderer } = require('electron');
function init() {
// add global variables to your web page
window.isElectron = true
window.ipcRenderer = ipcRenderer
}
init();
your web page
<script>
if (window.isElectron) {
window.ipcRenderer.send('ping', 'hello main')
window.ipcRenderer.on('pong', (event, msg) => console.log(msg))
}
</script>

Using preload script should work. You can use ipcRenderer to communicate with main process and expose it with simple API to renderer window. Simplest preload.js can look like:
const { ipcRenderer } = require('electron');
let listener;
const bridge = {
send: data => ipcRenderer.send('from-renderer', data),
onMessage: callback => listener = callback
}
ipcRenderer.on('to-renderer', (event, arg) => {
if (listener) {
listener(arg);
} else {
console.warn('No listener');
}
});
window.bridge = bridge;
in renderer
window.bridge.send('Data to main process');
window.bridge.onMessage(payload => console.log('Data received', payload))
Please also take a look at this discussion to get more info.

Related

Sending messages through Electron IPCmain channel to vue instance only works one way

I'm tyring to send messages back and forth between the main electron process and the vue instance. What i have so far is
Preload.js:
import { contextBridge, ipcRenderer } from 'electron'
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
window.ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld('ipcRenderer', {
//Render (Vue) to main (Electron)
send: (channel, data) => {
let validChannels = ['clientMessage'] // <-- Array of all ipcRenderer Channels used in the client
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
},
//Main (Electron) to Render (Vue)
on: (channel, func) => {
let validChannels = ['electronMessage'] // <-- Array of all ipcMain Channels used in the electron
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args))
}
}
})
So i have 2 whitelisted channels in ipcRender, one is called 'clientMessage' to send messaged from the vue instance to the electron main process, the other is 'electronMessage' to send messages from the electron main process to the vue instance.
In my background.js i have the following:
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
ipcMain.on('clientMessage', (event, args) => {
console.log('received a message from vue: '+args)
event.sender.send('message','return Message from electron'+args);
});
And in my app.Vue i have:
mounted () {
window.ipcRenderer.on('electronMessage', (event, data) => {
console.log('message from electron: '+data)
})
},
methods: {
sendMessage(){
window.ipcRenderer.send('clientMessage','testing')
}
}
The app runs fine, and when i call the sendMessage function I correctly get a console log on the electron terminal saying message received. So clearly vue -> electron messaging has worked, but why wont it work for the reverse?
Your contextBridge.exposeInMainWorld "keys" are ipcRenderer.send and ipcRenderer.receive.
You must use these keys to access your defined preload.js IPC methods. IE: send and on.
Specifically, to use the ipcRender.on(...) method, you call it with window.ipcRenderer.receive(...).
app.Vue (render thread)
// To send a message from render thread to main thread.
window.ipcRenderer.send('clientMessage','testing'); // Working
// To receive a message from main thread to render thread.
window.ipcRenderer.receive('electronMessage', (event, data) => {
console.log('message from electron: ' + data);
});
I figured out why, the way to send messages from electron main process to the renderer instance is not the same. Since there is only one electron main process running at all times, we can simple do window.ipcRenderer.send('', 'your message') in the vue renderer instance, but since there can be more than one renderer instances running, the main electron process needs to know which render process to send it to.
So in your main.js/background.js whatever .js you are running the electron main process out of. do this:
let win;
async function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: true,
preload: path.join(__dirname, "preload.js")
}
})
.
.
.
and then to send a message to the render instance:
win.webContents.send('<channelName>','your message')
that way it sends it to the correct render process. So my code in the end looks like this:
ipcMain.on('clientMessage', (event, args) => {
console.log('received a message from vue: '+args)
win.webContents.send('electronMessage','Reply from main process: '+args)
});

electron js how to call a function rendered in index

I'm going crazy to be able to call a function that I have rendered in an index and then execute that function with globalShortcut, I was researching and found that IPC could perform, but all the examples are to use functions from the main and not from the index. any idea that can guide me
I assume you have nodeIntegration set to true and contextIsolation set to false, as advised by the Electron documentation. To trigger a function in your renderer process from the main thread you can use ipcMain and ipcRenderer (these modules are part of Electron).
First, you need to expose an API for the renderer using contextBridge. This can be done with a preload.js file that has access to all modules like the main thread.
Example:
main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createMainWindow(){
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js'),
},
});
}
app.on('ready', () => {
createMainWindow();
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => {
// Whitelist channels
let validChannels = ['toMain'];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ['fromMain'];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});
Then, in your renderer process you can use window.api.send(channel, parameters); to send messages to the main thread and window.api.receive(channel, parameters); to listen for incoming ones. Similarly, you would use ipcMain.on(channel, (event, parameters) => { }); in the main process to handle messages from the renderer. However, to directly send messages to it, you have to use webContents, like so: mainWindow.webContents.send(channel, parameters);.
In your case, to execute a function in the renderer, you would call mainWindow.webContents.send('fromMain', 'trigger-function'); in the main process; and listen for this event in your renderer like so:
window.api.receive('fromMain', (parameters) => {
if (parameters === 'trigger-function'){
// Call your renderer function here
}
});

Why send method of Electron does not working from the main process?

I'm in big trouble. I checked other solutions but nothing helped me.
I don't understand why the "send" method doesn't work here. No exceptions, no messages or anything else.
I tried wrapping the send method in another method but it failed.
I tried using the "sendToFrame" method but it failed too.
main:
const { app, BrowserWindow, ipcMain } = require('electron')
let win;
function createWindow () {
win = new BrowserWindow({
width: 800,
height: 600,
frame: true,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: true,
contextIsolation: false, // false if you want to run 2e2 test with Spectron
enableRemoteModule: true,
}
});
win.loadFile('src/home.html');
win.webContents.openDevTools();
ipcMain.on('main', (event, msg) => {
sendData() // Does not work.
event.reply('new-client', 'res') // It works.
})
function sendData() {
win.webContents.send('new-client', {wsh: 'wshsdqsdqs'});
}
};
app.whenReady().then(createWindow);
Below, the "ipcRenderer.send" method works. It sends data to the main process but receives no response when I use the "webContents.send" method (in the main process).
new-client.js:
const { ipcRenderer } = require('electron');
const {resolve} = require('path');
const {Message} = require(resolve('src/js/shared/class/message/message.js'));
ipcRenderer.on('new-client', (event, arg) => {
console.log(event)
console.log(arg)
})
async function send() {
const msg = new Message(
'new-client',
'clients',
true
);
ipcRenderer.send('main', msg);
}
EDIT:
I forgot this...
I sent the data to the wrong window.
You can use BrowserWindow.getAllWindows, then send the data to a specific window.
Personally, I added an event when windows are open, in this way I can properly send data and manage windows.

ipcRenderer data not sent to ipcMain in Electron

I recently started developing desktop application using electron.
I want to send form details to main.js from the index.html on button click event. I have added a listener to the button in index.js. Searched online and found that I have to use ipcMain in main.js and ipcRenderer in index.js but the data is not being sent to ipcMain .
How do I get the form data in main.js ?
In index.html
<div class="btn-div fld">
<button id="loginButton" class="btn btn-primary st-btn" type="submit" name="button">Login</button>
</div>
<script src="index.js" charset="utf-8"></script>
In index.js
document.querySelector("#loginButton").addEventListener('click', function(){
userDetails = document.getElementById('login');
username = userDetails.username.value;
password = userDetails.password.value;
console.log("Hello");
const {ipcRenderer} = require('electron');
ipcRenderer.send('asynchronous-message', username);
})
In main.js
const { app, BrowserWindow , ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log( arg );
});
While creating a browser window in electron using new BrowserWindow(options) where options is an object. Define the object as:
options = {
webPreferences: {
preload: preload.js, //You need to create a file named preload.js (or any name) in your code
nodeIntegration: true,
contextIsolation: false,
}
}
Now in a new file called preload.js:
window.ipcRenderer = require('electron').ipcRenderer;
In your snippet you added const { app } ... which should be done this way to inject the javascript using a preload property in the object.
Now in the main app.js file (whatever you named maybe index.js) where you created the browser window:
const ipc = require('electron').ipcMain; //Add to your pre-existing code
ipc.on("close-app", (event, message) => { //"close-app" can be anything but, you need to use the same key in the send message side (later in this answer)
browserWindow.close(); //If you named the browserwindow as browserWindow
});
Now in your HTML (i.e., send message side)
...
<script>
window.ipcRenderer("close-app", ""); //Second parameter is used if you want to send some extra message. The extra message can be viewed in the server side from the message parameter in the app.js code (just above this paragraph)
</script>
This is a bit difficult if you are doing it for the first time.
I've added more articles which will help you clear your confusions:
A related answer at StackOverflow
Relation with socket.io communication in NodeJS
While I have seen that the other answer to this question may have worked for others, it did not work for me... I was using webpack and, for the life of me, could not get it to work even when adding ExternalsPlugin commonjs and electron. The following worked instead:
main.js
ipcMain.on("download", (event, info) => {
info.properties.onProgress = status => win.webContents.send("downloadProgress", status);
});
preload.js
contextBridge.exposeInMainWorld('electron', {
api: {
//receiving message from main.js
responseProgress: (channel, func) => {
let validChannels = ["downloadProgress"];
if (validChannels.includes(channel)) {
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
process: (channel) => {
//example for process that is called by button in reactjs etc
}
});
ReactComponent.js
function ReactComponent() {
useEffect(() => {
//retrieving information from preload.js originally sent from main.js
window.electron.api.responseProgress("downloadProgress", (progress) => {
console.log(progress);
console.log(progress.percent);
});
}, []);
return (
//example of calling api on button click
<button onClick={() => {
window.electron.api.process("toMain");
}}>Download</button>
)
}

Electron: How to securely inject global variable into BrowserWindow / BrowserView?

I want to load an external webpage in Electron using BrowserView. It has pretty much the same API as BrowserWindow.
const currentWindow = remote.getCurrentWindow();
const view = new remote.BrowserView({
webPreferences: {
// contextIsolation: true,
partition: 'my-view-partition',
enableRemoteModule: false,
nodeIntegration: false,
preload: `${__dirname}/preload.js`,
sandbox: true,
},
});
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');
In my preload.js file, I simply attach a variable to the global object.
process.once('loaded', () => {
global.baz = 'qux';
});
The app running on localhost:3000 is a React app which references the value like this:
const sharedString = global.baz || 'Not found';
The problem is I have to comment out the setting contextIsolation: true when creating the BrowserView. This exposes a security vulnerability.
Is it possible to (one way - from Electron to the webpage) inject variables into a BrowserView (or BrowserWindow) while still using contextIsolation to make the Electron environment isolated from any changes made to the global environment by the loaded content?
Update:
One possible approach could be intercepting the network protocol, but I'm not sure about this 🤔
app.on('ready', () => {
const { protocol } = session.fromPartition('my-partition')
protocol.interceptBufferProtocol('https', (req, callback) => {
if (req.uploadData) {
// How to handle file uploads?
callback()
return
}
// This is electron.net, docs: https://electronjs.org/docs/api/net
net
.request(req)
.on('response', (res) => {
const chunks = []
res.on('data', (chunk) => {
chunks.push(Buffer.from(chunk))
})
res.on('end', () => {
const blob = Buffer.concat(chunks)
const type = res.headers['content-type'] || []
if (type.includes('text/html') && blob.includes('<head>')) {
// FIXME?
const pos = blob.indexOf('<head>')
// inject contains the Buffer with the injected HTML script
callback(Buffer.concat([blob.slice(0, pos), inject, blob.slice(pos)]))
} else {
callback(blob)
}
})
})
.on('error', (err) => {
console.error('error', err)
callback()
})
.end()
})
})
After doing some digging, I found a few pull requests for Electron that detail the issue you are having. The first describes a reproducible example very similar to the problem you are describing.
Expected Behavior
https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content
A preload script should be able to attach anything to the window or document with contextIsolation: true.
Actual behavior
Anything attached to the window in the preload.js just disappears in the renderer.
It seems the final comment explains that the expected behavior no longer works
It was actually possible until recently, a PR with Isolated Worlds has changed the behavior.
The second has a user suggest what they have found to be their solution:
After many days of research and fiddling with the IPC, I've concluded that the best way is to go the protocol route.
I looked at the docs for BrowserWindow and BrowserView as well as an example that shows the behavior that you desire, but these PRs suggest this is no longer possible (along this route).
Possible Solution
Looking into the documentation, the webContents object you get from view.webContents has the function executeJavaScript, so you could try the following to set the global variable.
...
view.setAutoResize({ width: true, height: true });
view.webContents.loadURL('http://localhost:3000');
view.webContents.executeJavaScript("global.baz = 'qux';");
...
Other answers are outdated, use contextBridge be sure to use sendToHost() instead of send()
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.sendToHost('do-a-thing')
}
)
// Renderer (Main World)
window.electron.doThing()
So, executeJavaScript as suggested by Zapparatus ended up being part of the solution.
This is what's going on in renderer.js.
view.webContents.executeJavaScript(`
window.communicator = {
request: function(data) {
const url = 'prefix://?data=' + encodeURIComponent(JSON.stringify(data))
const req = new XMLHttpRequest()
req.open('GET', url)
req.send();
},
receive: function(data) {
alert('got: ' + JSON.stringify(data))
}
};
`)
const setContent = data => view.webContents.executeJavaScript(
`window.communicator.receive(${JSON.stringify(data)})`
)
ipcRenderer.on('communicator', (event, message) => {
setContent(`Hello, ${message}!`)
})
We ended up setting up a custom protocol, similar to how its been done here. In your main.js file set up the following:
const { app, session, protocol } = require('electron')
const { appWindows } = require('./main/app-run')
const { URL } = require('url')
protocol.registerSchemesAsPrivileged([
{
scheme: 'prefix',
privileges: {
bypassCSP: true, // ignore CSP, we won't need to patch CSP
secure: true // allow requests from https context
}
}
])
app.on('ready', () => {
const sess = session.fromPartition('my-view-partition')
// https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
sess.setPermissionRequestHandler((webContents, permission, callback) => {
// Denies the permissions request
const decision = false
return callback(decision)
})
sess.protocol.registerStringProtocol('prefix', (req, callback) => {
const url = new URL(req.url)
try {
const data = JSON.parse(url.searchParams.get('data'))
appWindows.main.webContents.send('prefix', data)
} catch (e) {
console.error('Could not parse prefix request!')
}
const response = {
mimeType: 'text/plain',
data: 'ok'
}
callback(response)
})
})
No preload.js or postMessage needed.

Categories

Resources