Noob to electronjs: Question about contextBridge / appropriate usage - javascript

I'm quite new to ElectronJS and much of my learning has come from examples and docs since it seems many tutorials out there are outdated especially due to the addition of enforcing nodeIntegration: false and contextIsolation: true.
My research has led me down the path of preload scripts, which by themselves still open themselves up to vulnerabilities, which then led me to contextBridge via preload scripts.
As a disclaimer, what I'm doing is probably unnecessary since I'm not actually connecting to any third-party and everything is local, so theoretically I could edit the webPreferences for nodeIntegration and contextIsolation. However, for my own learning purposes as well as best practice, I felt it necessary to understand how to do what I want to do and expose APIs such as require, fs, path, os etc. without using a "deprecated" feature.
Mainly wanted to post here to see if this is the proper method of using contextBridge.
main.js
const { app, BrowserWindow, Menu, ipcMain } = require("electron");
const path = require("path");
const os = require("os");
...
let mainWindow;
function createMainWindow() {
mainWindow = new BrowserWindow({
title: "Test",
icon: `${__dirname}/assets/icons/Icon_256x256.png`,
width: isDev ? 800 : 500,
height: 600,
resizable: isDev,
backgroundColor: "white",
webPreferences: {
preload: `${__dirname}/preload.js`,
nodeIntegration: false,
contextIsolation: true,
},
});
...
ipcMain.handle("get-file-path", async (event) => {
const filePath = path.join(os.homedir(), "test");
return filePath;
});
...
preload.js
const { ipcRenderer, contextBridge } = require("electron");
contextBridge.exposeInMainWorld("api", {
getFilePath: async () => {
const res = await ipcRenderer.invoke("get-file-path");
return res;
},
});
renderer.js
window.addEventListener("DOMContentLoaded", async () => {
document.getElementById("output-path").innerText =
await window.api.getFilePath();
});
It works as intended. From my understanding of what is happening here is by using contextBridge with a callback method I'm essentially keeping everything isolated within that function and not exposing anymore that what's within that closure--is that correct?
Once again, I'm really new (1 day into electron essentially). I've only been 'developing' for 7 months or so.
Would this also be valid.. offloading to another js file and exposing those functions through context bridge?
test.js
const path = require("path");
const os = require("os");
const getFilePath = () => {
const filePath = path.join(os.homedir(), "test");
return filePath;
};
module.exports = {
path: getFilePath,
};
preload.js
const { contextBridge } = require("electron");
const mainFunctions = require("./test.js");
const { path } = mainFunctions;
contextBridge.exposeInMainWorld("api", {
getFilePath: path(),
});
Both give me the outcome I expect.

Related

how to use webContents.send function of electron.js in other ipc render file?

I am trying to separate the IPC function from the main.js file in electron because it gets too long
how can I use this webContents.send in different js file not in main.js
mainWindow.webContents.send("search",recordset.recordset)
it shows this error
Cannot read properties of undefined (reading 'webContents')
Separation of concerns will be your number one priority here. To achieve this, you can use setters and getters.
Remember, when Node first require's a module, it is also cached. Let's use this advantage as a form of state management.
Prior to separating / refactoring your code, you will find that your main.js file can grow to an enormous size. Using techniques such as this will allow you to split up your code into easily manageable, readable, single responsibility segments of code.
If you haven’t done so already, let's move construction of your mainWindow out of the main.js file and into its own file.
main.js (main thread)
// Import the necessary Electron modules.
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
// Import the necessary Node modules.
const nodePath = require('path');
// Import the necessary Application modules.
const appMainWindow = require(nodePath.join(__dirname, 'main-window'));
// Prevent garbage collection.
let mainWindow = null;
// Application is now ready to start.
electronApp.on('ready', () => {
mainWindow = appMainWindow.create();
});
// Re-activate Application when in macOS dock.
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
appMainWindow.create(mainWindow);
}
});
// Close Application completely if not on macOS.
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
Having management of the mainWindow object in its own file separates our code into more readable and manageable chunks.
main-window.js (main thread)
// Import the necessary Electron modules.
const electronBrowserWindow = require('electron').BrowserWindow;
// Define the main window.
let mainWindow;
// Create the main window.
function create() {
mainWindow = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
// Load the main window.
mainWindow.loadFile(nodePath.join(__dirname, 'main-window.html'))
.then(() => { mainWindow.show(); })
return mainWindow;
}
// Get the main window object.
function get() {
return mainWindow;
}
// Export the publicly available functions.
module.exports = {create, get};
Finally, in your file (or any other file) that requires reference to your mainWindow object, just require your main-window.js file and call the get() function.
any-file (main thread)
// Import the necessary Node modules.
const nodePath = require('path');
// Import the necessary Application modules.
const appMainWindow = require(nodePath.join(__dirname, 'main-window'));
// Lets use it.
appMainWindow.get().webContents.send("search", recordset.recordset);

Electron property inacessible in render.js

I'm trying to learn how to build electron apps and I found this Youtube tutorial Build a Desktop App with Electron... But Should You? on building a simple screen recording app but i encountered this error
Uncaught TypeError: Cannot destructure property 'Menu' of 'remote' as it is undefined.
at render.js:9
Here is the exact code from the tutorial code
const videoElement = document.querySelector('video');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const videoSelectBtn = document.getElementById('videoSelectBtn');
videoSelectBtn.onclick = getVideoSources;
const {desktopCapturer,remote} = require('electron');
const {Menu} = remote;
async function getVideoSources(){
const inputSources = await desktopCapturer.getSources({
types:['window','screen']
});
const videoOptionsMenu = Menu.buildFromTemplate(
inputSources.map(source =>{
return{
label:source.name,
click:()=>selectSource(souce)
}
})
);
videoOptionsMenu.popup();
}
What I'm I doing wrong?
in new version of electron. you need to allow your electron app to use remote modules.
add enableRemoteModule: true flag in your main electron code.
mainWindow = new BrowserWindow({
width: 1280,
height: 960,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
},
});

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.

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

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.

How to use electron's app.getPath() to store data?

I want to store images on the users computer, so I figure it should be stored in users data folder, as described here.
app.getPath(name)
name. Returns String - A path to a special directory or file associated with name. On failure an Error is thrown. You can request the following paths by the name:
home User's home directory
appData Per-user application data directory, which by default points to:
%APPDATA% on Windows
$XDG_CONFIG_HOME or ~/.config on Linux
~/Library/Application Support on macOS
userData The directory for storing your app's configuration files, which by default it is the appData directory appended with your app's name.
...
This is what I think you're supposed to do:
const app = require('electron');
alert(app.getPath('userData'));
But I get "getPath is not a function". I am not sure where to put it. It does not work from my html file or the renderer file, and I'm not sure how to use it from the main file because that's not linked to the web page.
Since the remote method is being considered deprecated, as shown here, I'd suggest you do this:
const {app} = require('electron');
console.log(app.getPath('userData'));
remote is considered dangerous.
app.getPath will be always available in main process.
Here is how to do it in renderer process without using remote (electron 7+)
Basically you have to use invoke in renderer.
in main
ipcMain.handle('read-user-data', async (event, fileName) => {
const path = electron.app.getPath('userData');
const buf = await fs.promises.readFile(`${path}/${fileName}`));
return buf;
})
in renderer
ipcRenderer.invoke('read-user-data', 'fileName.txt').then(
result => doSomething()
);
Here is what I use when I need to switch between dev and release
const electron = require('electron');
export const userDataPath = (electron.app || electron.remote.app).getPath(
'userData'
);
Another way to prevent the error "getPath is not a function" is to make the code work both in the renderer process and the main process:
const electron = require('electron');
const configDir = (electron.app || electron.remote.app).getPath('userData');
console.log(configDir);
I had trouble with app.getPath('userData') to save/load config files, etc and ended up using OS specific env vars in the meantime:
const getAppBasePath = () => {
//dev
if (process.env.RUN_ENV === 'development') return './'
if (!process.platform || !['win32', 'darwin'].includes(process.platform)) {
console.error(`Unsupported OS: ${process.platform}`)
return './'
}
//prod
if (process.platform === 'darwin') {
console.log('Mac OS detected')
return `/Users/${process.env.USER}/Library/Application\ Support/${YOUR_APP_NAME}/`
} else if (process.platform === 'win32') {
console.log('Windows OS detected')
return `${process.env.LOCALAPPDATA}\\${YOUR_APP_NAME}\\`
}
}
if you wanna do it in renderer process
try this,it is work for me
// main.js
const electron = require('electron')
const electronRemote = process.type === 'browser' ? electron :
require('#electron/remote')
const { app, ipcMain, Menu, globalShortcut } = require('electron')
const BrowserWindow = electronRemote.BrowserWindow
const isDev= require('electron-is-dev')
const { initialize, enable } = require('#electron/remote/main')
initialize()
let mainWindow
app.on('ready', ()=>{
mainWindow = new BrowserWindow({
width: 1024,
height: 600,
minWidth:600,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
})
enable(mainWindow.webContents)
})
render process
// render process
const { app } = window.require('#electron/remote')
const savedPath = app.getPath('userData')

Categories

Resources