How to organize Electron.js renderer code across multiple files without compromising security? - javascript

I'm building an Electron.js application, and I have been reading the documentation on context isolation, the main and renderer processes, and more.
I am struggling to understand how I can organize my renderer process code into multiple files or classes without using other frameworks such as Angular or Vue.
I have more or less achieved this goal by setting the sandbox option to false, which allows me to use require for external modules that are not part of Electron. However, as far as I understand, this is not a best practice because it poses security risks. Additionally, I encountered difficulties with classes when using this option. With only functions it works.
Here is the code that is giving me errors. How can I organize my renderer code into multiple files without going crazy?
main.js
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const nodePath = require("path");
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
}
ipcMain.handle('open-dialog', async (event, arg) => {
const result = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections']
});
return result;
});
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
preload.js
const { contextBridge, ipcRenderer } = require('electron');
const Test = require('./test');
contextBridge.exposeInMainWorld('myAPI', {
Test: Test
});
renderer.js
const api = window.myAPI;
const button = document.querySelector('#hi');
button.addEventListener('click', async () => {
const test = new api.Test();
test.hi();
});
test.js file where I put the functions or classes to be exported
class Test {
constructor() {
this.name = "TheSmith1222";
}
hi(){
console.log("Hi, " + this.name);
}
}
module.exports = {
Test
};
index.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<button id="hi">Hi</button>
<script src="./renderer.js" type="module"></script>
</body>
</html>
Output button onclick:
renderer.js:5 Uncaught (in promise) TypeError: api.Test is not a constructor
at HTMLButtonElement.<anonymous> (renderer.js:5:15)

Related

Opening files/folders using Electron and ContextIsolation

I have been trying to build an app for file managing and different stuff for work. One of the feautures is to open links in windows file explorer, but I am not able to do it. These are the files isolated from the rest of the app code:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
<p id="info"></p>
<div>
<button id="open-file-manager">View Demo</button>
</div>
</body>
<script src="./renderer.js"></script>
</html>
main:
const { app, BrowserWindow } = require('electron');
const path = require('path');
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
win.loadFile('index.html');
};
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
preload:
const { contextBridge } = require('electron');
const { shell } = require('electron');
contextBridge.exposeInMainWorld('bridge', {
electron: () => process.versions.electron,
buttonClick: (link) => shell.openPath(link)
});
renderer:
const information = document.getElementById('info');
information.innerText = `Electron (v${bridge.electron()})`;
const fileManagerBtn = document.getElementById('open-file-manager');
fileManagerBtn.addEventListener('click', (event) => {
bridge.buttonClick('C:/Users')
});
Error when clicking the button once the app has been launched:
Uncaught Error: Cannot read properties of undefined (reading 'openPath')
at HTMLButtonElement.<anonymous> (renderer.js:7:10)
I tried with ContextIsolation false and it worked with little modifaction on renderer so I must be missing something. Please help me in this, I have tried to search for an answer but I am not able to find it.
I finally caught what was happening to my app when I reread the electron documentation. It seems that "shell" cannot be used in "sandboxed" scripts and guess what, since electron v20.0.0 the preload script is a sandboxed script.
Main process can load shell commands so with IPC methods I was able to launch file explorer without deactivating ContextIsolation.

How do I wait to update an HTML header until promise is resolved?

I am building my first electron application and I am running into a JS error I am not sure how to fix. I have managed to setup the IPC for a basic password manager application. The use-case is simple:
I click an HTML button and the frontend connects to the backend and delivers a keyword from which to build a password.
Password is generated.
Upon completion, the password is returned to the frontend to be displayed via HTML in a header tag.
Here is an example of the expected behavior with the input string dog:
keyword --> dog
generated password --> dog-jdls4ksls
The issue I am seeing is that instead of printing the generated password, I am seeing:
[object Object]-jdls4ksls
My best guess is that, since I am using async/await, I am printing the promise memory object instead of the returned value. I do not know, however, how I would block to wait for completion. The code in question providing this output is the last line of render.js, which was called from the HTML body.
Any help would be appreciated!
For context, I am primarily a backend developer, with plenty of python and C/C++/C# experience. My goal is to rebuild a C#/.NET GUI application into an electron app.
Here is all my code.
main.js
const {app, BrowserWindow, ipcMain} = require("electron")
const path = require("path")
function generatePassword(keyword) {
console.log(keyword)
return keyword + '-' + Math.random().toString(36).substring(2,12)
}
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
resizable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('html/passwordGen.html')
}
app.whenReady().then(() => {
ipcMain.handle("generatePassword", generatePassword)
// console.log(generatePassword('test string')) // works
createWindow()
}).catch(error => {
console.log(error) // log error to console
app.quit() // quit the app
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('main', {
genPW: (keyword) => ipcRenderer.invoke("geåneratePassword", keyword)
})
render.js
async function testClick () {
const pw_root = document.getElementById("keyword")
const pw_label = document.querySelector("#password")
pw_label.innerText = await window.main.genPW(pw_root.value)
}
passwordGen.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Generator</title>
<link rel="stylesheet" href="../css/style.css">
<script src="../render.js"></script>
</head>
<body>
<h1>Password Generator</h1>
<input type="text" id="keyword" placeholder="Please enter a keyword...">
<button id="btn" onclick="testClick()">Generate Password</button>
<h1 id="password"></h1>
</body>
</html>
Edit:
Here is the code that worked. The accepted solution worked with the exception that I needed to either 1) keep the async/await on the generatePassword() or 2) convert it to .then() format as recommended in another solution.
main.js
const {app, BrowserWindow, ipcMain} = require("electron")
const path = require("path")
function generatePassword(keyword) {
console.log(keyword)
return keyword + '-' + Math.random().toString(36).substring(2,12)
}
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
resizable: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('html/passwordGen.html')
}
app.whenReady().then(() => {
// ipcMain.handle("generatePassword", generatePassword)
// console.log(generatePassword('stink')) // works
ipcMain.handle('generatePassword', (_event, keyword) => {
console.log(keyword); // Testing
return generatePassword(keyword);
});
createWindow()
}).catch(error => {
console.log(error) // log error to console
app.quit() // quit the app
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('main', {
genPW: (keyword) => {
return ipcRenderer.invoke("generatePassword", keyword)
}
})
render.js
async function testClick () {
const pw_root = document.getElementById("keyword")
const pw_label = document.querySelector("#password")
pw_label.innerText = await window.main.genPW(pw_root.value)
// window.main.genPW(pw_root.value).then(res => {pw_label.innerText = res})
// ^^^ works as well if async/await removed
}
You were really close. Using Elctron's invoke method is the correct approach.
Within your main.js file, Electron's IPC handle signature contains the channel and listener arguments. In
your code you are calling your generatePassword() function in place of the listener argument. Instead, it should
be (event, ...args). In your specific case (event, keyword).
See ipcMain.handle(channel, listener)
for more information.
Additionally, within your preload.js script, all you need to do is add a return statement in front of your ipcRenderer.invoke method.
Finally, there is no need to use async on your testClick() function. Electron's invoke handles all of this.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require('path');
// Prevent garbage collection
let window;
function createWindow() {
window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
resizable: false,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// ---
function generatePassword(keyword) {
console.log(keyword)
return keyword + '-' + Math.random().toString(36).substring(2,12)
}
electronIpcMain.handle('generatePassword', (event, keyword) => {
console.log(keyword); // Testing
return generatePassword(keyword);
});
preload.js (main process)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
contextBridge.exposeInMainWorld(
'main', {
genPW: (keyword) => {
return ipcRenderer.invoke('generatePassword', keyword);
}
});
For sake of simplicity, I have included your render.js script within <script> tags below the closing </body> tag.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Emergency</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<div>
<label for="password">Password:</label>
<input type="text" id="password">
<input type="button" id="submit" value="Submit">
</div>
<div>
<label for="generated-password">Generated Password:</label>
<input type="text" id="generated-password" disabled>
</div>
</body>
<script>
document.getElementById('submit').addEventListener('click', () => {
window.main.genPW(document.getElementById('password').value)
.then((generatedPassword) => {
document.getElementById('generated-password').value = generatedPassword;
})
})
</script>
</html>
I dont think you can use async function as event listener, you need to use regular (not async) function here.
function testClick() {
const pw_root = document.getElementById("keyword")
const pw_label = document.querySelector("#password")
window.main.genPW(pw_root.value).then(res => {pw_label.innerText = res})
}
also, you have typo here: invoke("geåneratePassword")
Try using double await as in
await (await callToBackend)

document.getElementById("myFile").value gets undefined using electron

I have a very basic html file (using electron);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> File Uploader </title>
<link rel="stylesheet" href="style.css">
<script defer src="render.js"></script>
</head>
<body>
<h1>Drive File Uploader</h1>
<input type="file" id="myFile" name="myFile">
<button onclick="FileUploadPing()">Upload your file</button>
</body>
</html>
and an event listener named render.js;
const ipcRenderer = require("electron").ipcRenderer;
const FileUploadPing = () => {
var input = document.getElementById("myFile").value
if (input) {
ipcRenderer.send("FileUploadPing",inputVal);
}else{console.log("no path value")}
};
ipcRenderer.on("FileRecievePing", (event, data) => {
alert(data)
});
But when I click submit, document.getElementById("myFile").value returns undefined
how can I pull that value?
This is an interesting issue that confronts many people using Electron. One could either use (via Electron) the native OS dialog.showOpenDialog([browserWindow, ]options) dialog or the html <input type="file"> tag.
To circumvent the need to manage the is prefixed with C:\fakepath\ issue, it is often better to use the native approach. After all, that is what Electron is best at.
Let me show you how to quickly set up a html button that when clicked, will open the native file selector dialog, and when a path is selected, return the path to the render thread for display.
In the below code, we will be using a preload.js script configured to communicate (using IPC) between the main thread and render thread(s). Context Isolation will describe this in more detail.
First off, let's code the main.js file which will include the creation of the native file dialog.
main.js (main thread)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const electronDialog = require('electron').dialog;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
// Prevent garbage collection
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 1000,
height: 700,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); })
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Open the file dialog
electronIpcMain.on('message:openDialog', (event) => {
let options = {
title: 'Select File',
properties: ['openFile']
};
electronDialog.showOpenDialog(window, options)
.then((result) => {
if (result.canceled) {
console.log('User cancelled file dialog.');
return;
}
event.reply('message:dialogPath', result.filePaths[0]);
})
.catch((error) => { console.error(error); });
})
Now, let's create the index.html file, which for the sake of simplicity, also includes the Javascript within the <script> tags.
NB: Instead of deferring your script in the <head>, you can include it just before the closing <html> tag. Placing it here effectively performs the same thing as defer in the <head> <script> tag.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Drive File Uploader</title>
</head>
<body>
<div>Drive File Uploader</div>
<hr>
<label for="path">Path: </label>
<input type="text" id="path" name="path">
<input type="button" id="open-dialog" name="open-dialog" value="...">
<input type="button" id="upload" value="Upload">
</body>
<script>
// Let's declare it as it is used more than once
let filePath = document.getElementById('path');
// Event listeners
document.getElementById('open-dialog').addEventListener('click', () => { window.ipcRender.send('message:openDialog'); });
document.getElementById('upload').addEventListener('click', () => { console.log(filePath.value); });
// IPC message from the main thread
window.ipcRender.receive('message:dialogPath', (path) => { filePath.value = path; })
</script>
</html>
Finally, let's add the preload.js script to allow the main thread and render thread(s) to safely communicate with each other.
Note: This is where we define our whitelisted channel names.
preload.js (main thread)
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [
'message:openDialog'
],
// From main to render.
'receive': [
'message:dialogPath'
],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Hopefully, the above outlines how simple it can be to use (via Electron) the native dialog boxes. The benefit being they have OS specific functionality and feel.
I dont know what I did different, but when I tried this it suddenly worked.
main.js;
const { app, BrowserWindow, ipcMain } = require("electron");
let win = null;
const createWindow = () => {
win = new BrowserWindow({
width: 800,
height: 600,
resizable: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
nativeWindowOpen: true,
},
});
win.loadFile("index.html");
};
app.whenReady().then(createWindow);
render.js;
const ipcRenderer = require("electron").ipcRenderer;
const {uploadFileToCloud} = require("C:/Users/efeba/desktop/pythonAndJS/ITGSProject/app.js")
const FileUploadPing = () => {
var name = document.getElementById("myFile").value
name = name.replace("C:\\fakepath\\","")
console.log(name)
var path = document.getElementById("myFile").files[0].path
path = path.replace(name,"")
path = path.replaceAll("\\", "/") // beautify the string
console.log(path)
if (path) {
uploadFileToCloud(name, path)
alert("File Uploaded")
}else{console.log("no path value")}
};
//file uploads but content is empty
//I suspect pulling the libraries in renderer has some problems, will
//try to make it by sending a ping to main.js
index.html;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> File Uploader </title>
<link rel="stylesheet" href="style.css">
<script defer src="render.js"></script>
</head>
<body>
<h1>Drive File Uploader</h1>
<input type="file" id="myFile" name="myFile">
<button onclick="FileUploadPing()">Upload your file</button>
</body>
</html>
Hope I could help

`loadURL` in Electron has error "window.require is not a function" only this website

NOTE: Electron 17.0.1
I'm so confuse about to use the win.loadURL function in Electron.
For main purpose, to create a dark theme for chat application.
I want to loadURL in Electron and inject CSS to change to dark theme.
BUT when I use loadURL and then run Electron app.
It will show an error "window.require is not a function"
and "Uncaught TypeError: Cannot read properties of undefined (reading 'instance')"
here is my code:
// main.js
const { app, BrowserWindow } = require("electron");
const path = require("path");
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
});
win.webContents.openDevTools();
win.loadURL("https://seatalkweb.com/");
}
app.whenReady().then(() => {
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
// preload.js
window.addEventListener("DOMContentLoaded", () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
};
for (const type of ["chrome", "node", "electron"]) {
replaceText(`${type}-version`, process.versions[type]);
}
});
and this is an error:
I do not know why it has an error only this website when open in electron? (no problem if open in chrome)
this website is https://seatalkweb.com/
and it fine if I use other URL instead (such as google.com, facebook.com, etc.)
How can I fix it ?
and could somebody can explain me about this problem ?
Seems weird like a require is called is your page but it's maybe an Electron call du to the LoadUrl... If it's a require problem you may need to activate the Node integration..
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
nodeIntegration: true
},
});
If it's not from Electron Node integration, then in may be a problem in your web page script.

Electron showOpenDialog arrow function (event.send) not working

I'm following the dialog example for opening files from: https://github.com/electron/electron-api-demos
I copied the code from the example. The open file dialog does in fact work and I'm able to select a file but can't figure out why the arrow function to send the file path back to renderer doesn't work(nothing is logged with console.log).
Can anyone spot what's wrong?
The project was started using electron-forge and my OS is linux.
Thanks
index.js
const { app, BrowserWindow, ipcMain, dialog, } = require('electron');
require('electron-reload')(__dirname);
const path = require('path');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit();
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'index.html'));
// Open the DevTools.
mainWindow.webContents.openDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
ipcMain.on('open-file-dialog', (event) => {
dialog.showOpenDialog(
{
properties: ['openFile',]
},
(files) => {
console.log('ok')
if (files) {
event.sender.send('select-file', files)
}
})
})
index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div>
<div>
<button class="demo-button" id="select-directory">Select file</button>
<span class="demo-response" id="selected-file"></span>
</div>
<br><br>
</div>
<script>
const electron = require('electron')
const { ipcRenderer } = electron
const selectDirBtn = document.getElementById('select-directory')
selectDirBtn.addEventListener('click', (event) => {
ipcRenderer.send('open-file-dialog')
})
ipcRenderer.on('select-file', (event, path) => {
console.log(path)
document.getElementById('selected-file').innerHTML = `You selected: ${path}`
})
</script>
</body>
</html>
The dialog API has been modified with the release of Electron 6.
dialog.showOpenDialog() and other dialog functions now return promises and no longer take callback functions. There also are synchronous counterparts which return the selection result in a blocking fashion, e.g. dialog.showOpenDialogSync().
Example usage (in renderer process)
const remote = require("electron").remote
const dialog = remote.dialog
dialog.showOpenDialog(remote.getCurrentWindow(), {
properties: ["openFile", "multiSelections"]
}).then(result => {
if (result.canceled === false) {
console.log("Selected file paths:")
console.log(result.filePaths)
}
}).catch(err => {
console.log(err)
})
As of February 2020, the electron-api-demos use Electron 5. That is why their dialog calling code still uses the old form.

Categories

Resources