How can I add a custom chrome extension to my Electron app? - javascript

I am facing some trouble adding chrome addons into my Electron BrowserWindow.
Before creating my window (and after the ready event has fired), I try to add a devtools extension that my browser needs to do screen sharing.
BrowserWindow.addDevToolsExtension('/home/USER/.config/chromium/Default/Extensions/dkjdkjlcilokfaigbckcipicchgoazeg/1.5_0');
I followed this Electron guide, and it worked for their example (adding the react develop tool). When I do the exact same thing with my own chrome extension I have this error:
[4735:1116/163422.268391:ERROR:CONSOLE(7701)] "Skipping extension with invalid URL: chrome-extension://extension-name", source: chrome-devtools://devtools/bundled/shell.js (7701)
I don't really get why the error specified is "invalid URL" since I'm doing the exact same thing / process with the react addon without a problem. I also have no idea what to do. Is it possible that my chrome addon is not Electron-compatible?

It looks like you're trying to add a regular Chrome extension instead of a Dev Tools extension.
The BrowserWindow.addExtension(path) method is for regular Chrome extensions:
BrowserWindow.addExtension(path)
path String
Adds Chrome extension located at path, and returns extension's name.
The method will also not return if the extension's manifest is missing or incomplete.
Note: This API cannot be called before the ready event of the app module is emitted.
- https://electronjs.org/docs/api/browser-window#browserwindowaddextensionpath
Conversely, the BrowserWindow.addDevToolsExtension(path) method is for Dev Tools extensions:
BrowserWindow.addDevToolsExtension(path)
path String
Adds DevTools extension located at path, and returns extension's name.
The extension will be remembered so you only need to call this API once, this API is not for programming use. If you try to add an extension that has already been loaded, this method will not return and instead log a warning to the console.
The method will also not return if the extension's manifest is missing or incomplete.
Note: This API cannot be called before the ready event of the app module is emitted.
- https://electronjs.org/docs/api/browser-window#browserwindowadddevtoolsextensionpath
Note that in both cases you need to wait for the ready event from the app module to be emitted:
const { BrowserWindow, app } = require('electron')
let mainWindow = null
function main() {
BrowserWindow.addExtension('/path/to/extension')
mainWindow = new BrowserWindow()
mainWindow.loadURL('https://google.com')
mainWindow.on('close', event => {
mainWindow = null
})
}
app.on('ready', main)

Support for Chromium extensions in Electron is actively being worked on at the moment. The support isn't complete yet, but the GitHub issue seems to have regular updates being posted.
Fingers crossed!
A current pull request is open for 'just enough extensions [api] to load a simple ... extension'

Electron 9 has much more support for extensions!
To load them, use session.loadExtension: https://github.com/electron/electron/blob/master/docs/api/extensions.md
const { app, BrowserWindow, session } = require('electron')
// ... in your createWindow function, which is called after app.whenReady
const mainWindow = new BrowserWindow({...})
const ext = await session.defaultSession.loadExtension('/path/to/unpacked/chrome-ext')
console.log('ext', ext)
// outputs config file
// {
// id: 'dcpdbjjnmhhlnlbibpeeiambicbbndim',
// name: 'Up! – Free Social Bot',
// path: '/Users/caffeinum/Development/GramUp/chrome-ext',
// url: 'chrome-extension://dcpdbjjnmhhlnlbibpeeiambicbbndim/',
// version: '1.7.0',
// manifest: { ... }
// }
Read more: https://github.com/electron/electron/blob/master/docs/api/extensions.md
Also, there's another project that helps you do that, also adds additional functionality: https://github.com/sentialx/electron-extensions

While there is a documented method to register a normal extension, in majority of cases it won't do much, as Electron supports only an accessibility subset of the chrome.* APIs (apparently only the stuff required by Spectron and Devtron) and as they've stated a while ago, they don't have any plans to support Chrome extension APIs at a full scale.

Related

os.platform returns browser instead of actual OS - React & Electron

I am trying to retrieve the appdata folder location for the application, and since each os has a separate path for the appdata or application support folder, I tried retrieving the os type to specify which path to use. The issue is os.platform() returns 'browser'. I have tried running it on windows and mac, but they all return browser. If i run process.platform in electron.js it gives me the correct os, but in react I get browser. How can I get the proper OS?
In a browser you can use a combination of navigator.platform, navigator.userAgent, and navigator.userAgentData.platform to get the information you want, but it might take some testing and parsing.
AFAIK, navigator.userAgentData.platform is available only on Chrome/Chromium-based browsers, but gives the most straight-forward result when available.
Checking which platform you're using, rather than checking for specific features, is generally consider not to be a good idea -- but I've found it hard to avoid sometimes myself, especially when working around platform-specific quirks and bugs.
This is because you are running process.platform in the renderer process.
In order to get the correct value you need to run platform.process either on main process (usually the background.js file) or via #electron/remote, like this:
window.require('#electron/remote').process.platform
#electron/remote's usage depends on your electron version, I recommend you to check #electron/remote readme.
Have you tried using Electron's app.getPath(name) method?
This method will return the users application data directory.
Only once the app is "ready" can you retrieve this path.
// Electron module(s).
const electronApp = require('electron').app;
// Application is now ready to start.
electronApp.on('ready', () => {
// Get the users app data directory.
let appData = electronApp.getPath('appData');
// Get the users home directory.
let home = electronApp.getPath('home');
})

Use AudioWorklet within electron (DOMException: The user aborted a request)

I am trying to use an AudioWorklet within my electron app for metering etc. which works fine when executed in dev mode where the worklet is being served by an express dev server like http://localhost:3000/processor.js.
However if I try to run the app in prod mode the file is being served locally like file://tmp/etc/etc/build/processor.js and in the developer-console I can even see the file correctly being previewed but I get this error message:
Uncaught (in promise) DOMException: The user aborted a request.
I saw that someone else had a similar problem before over here but unfortunately my reputation on stack overflow is not high enough to comment directly. The suggestion there to change the mime-type to application/javascript or text/javascript sounds good but I have no idea how to force electron to use a specific mime-type for a specific file. Furthermore in the developer-console in the network tab it seems like chromium is actually already assuming a javascript file for my processor.js.
I already tried to load the worklet with a custom protocol like that
protocol.registerStandardSchemes(['worklet']);
app.on('ready', () => {
protocol.registerHttpProtocol('worklet', (req, cb) => {
fs.readFile(req.url.replace('worklet://', ''), (err, data) => {
cb({ mimeType: 'text/javascript', data });
});
});
});
and then when adding the worklet
await ctx.audioWorklet.addModule('worklet://processor.js');
unfortunately this only ends in these errors followed by the first error
GET worklet://processor.js/ 0 ()
Uncaught Error: The error you provided does not contain a stack trace.
...
I found a hacky solution if anybody is interested.
To force a mime-type electron / chromium is happy with I load the worklet file with the file api as a string, convert it to a blob with mime-type text/javascript and then create an object url from that
const processorPath = isDevMode ? 'public/processor.js' : `${global.__dirname}/processor.js`;
const processorSource = await readFile(processorPath); // just a promisified version of fs.readFile
const processorBlob = new Blob([processorSource.toString()], { type: 'text/javascript' });
const processorURL = URL.createObjectURL(processorBlob);
await ctx.audioWorklet.addModule(processorURL);
Hope this helps anyone having the same problem...
If you're using webpack to compile your source you should be able to use the web-worker loader for your custom worker scripts.

Electron: webview doesn't receive callback code from new created window

I've got desktop application that works as simple browser. It has tabs with webviews that show web pages.
Some websites have social sign-in option. And I really need it to be available for my users.
In general browser (e.g. chrome) when you sign in via Facebook, it creates new window with social authorization form. After submitting this form the new window closes and parent window receives callback code, reloads and sign you in.
In my app when you try to do the same, webview that contains the web page, creates new BrowserWindow with the same auth form. At this point everything works fine. When you submit form, the new window closes but nothing happens after. Parent window doesn't receive any callback, the code that should run isn't triggered at all. Or probably it has been received, but not triggered, since if I reload page, it shows that I'm signed in.
According to the electron documentation <webview> runs in a separate process than renderer process. So I think when new window closes, the callback is received by parent BrowserWindow (renderer process) and not runs exactly in webview frame.
While searching through different sources for solution, I found that this problem is also concerned with window.opener, that was not fully supported in the earliest versions of electron. But according to the issue#1865 on github full window.opener support is now available if I open window by window.open method.
To make this work I should prevent webview to open new BrowserWindow and create it by myself.
The following code fails:
// in renderer process
webview.addEventListener('new-window', function(event) {
event.preventDefault();
// I also tried 'event.defaultPrevented = true;'
// or 'return false;'
});
The listener is triggered, but I can't prevent new-window opening.
Someone wrote that new-window can be prevented in main process only.
So I add the following code in my main.js:
// in main process
var electron = require('electron');
var BrowserWindow = electron.BrowserWindow;
mainWindow = new BrowserWindow({
"width": 970,
"height": 500,
"center": true,
'title': 'Main window',
"autoHideMenuBar": true
});
mainWindow.loadURL('file://' + root + '/index.html');
mainWindow.webContents.on('new-window', function(event) {
console.log(event);
});
This time event 'new-window' is not triggered at all. I think this is because the event works only if renderer process requests to open a new window by window.open for example. But in my case new window is opened by <webview> tag, not by renderer process. So I failed to prevent new-window to open since these are two separate processes.
So I decided just to get new-window after it opens.
// in renderer process
var electron = require("electron");
var remote = electron.remote;
webview.addEventListener('new-window', function(event) {
var win = remote.BrowserWindow.getFocusedWindow();
var contents = win.webContents;
win.setIcon(root+"/img/favicon.ico");
win.setMenuBarVisibility(false);
contents.on('dom-ready', function() {
contents.openDevTools();
})
});
After that I really have no idea what to do next... how to append window.opener and solve my issue
I found many similar issues on github. But all of them are merged to one issue #1865 and there is no clear answer so far.
I know solution exists. Brave developers fixed this. Brave browser also made with electron works correctly the same as general browser does. But I can't find answer while looking through its sources.
They even made tests to check whether window.opener works correctly. But I failed to install Brave from git repository. After 'npm-start' it says 'Error: %1 is not valid Win32 application'. I have Windows 10. (just another reason makes me switch to Linux. Hope to make it soon.)
So, my question is: how to send callback data from new window back to webview that created this window. Thanks in advance.
First, to handles new-window popups yourself, you have to disable allowpopups (i.e. remove allowpopups attribute from the webview)
This way the new-window event will be triggered but no popup will be opened until you explicitelly code it.
For the window.opener feature which is required for some social or sso login, electron does not have enough support of it. See issue 1865
The Brave team have implemented it in a restricted way in their fork of Electron. See issue 4175.
You may try to either:
Use the fork of electron from Brave (not sure it will fit your needs as they have made some major changes)
Make a PR to electron to integrate the support of window.opener from the fork.
Make your own polyfill for the methods you need by injecting a script that implements missing window.opener methods. You can use window.opener.send to communicate with renderer process)

How to write file in Google Chrome App without prompting?

I am fumbling around with the free Chrome Dev Editor on my Chromebook. I am trying to use the fileSystem to read and write .txt files. It is all very wrapped up, not at all like in C. I can no more tell if I am even allowed to do something, let alone where the proper place is to find out how.
I think the files I can see using the Files thingy are in the sandbox that I am allowed to play in (meaning, folders that are accessible by the app?) The root is called Downloads. Sure enough, if I use all the dot calls and callback arguments for the read, as in the examples at developer.chrome.com/apps/filesystem, it works. But I have to have a prompt
every time for both reads and writes.
A little more Googling came up with this trick: (I think it was here in stackoverflow, in fact) a chrome.runtime call, getPackagedDirectoryEntry, that seems to give me a handle to the folder of my app. Great! That's all I need to not have to go through the prompting. For the readfile, anyway.
But then trying to apply the same trick to the writefile did not work. In fact, it did nothing discernible. No errors, no complaints. Nothing. Even though the write file with prompting works fine (so presumably I have the permissions and Blob construction right.) What to do?
Here is my code:
function test(){
// Samsung 303C Chromebook - Chrome Dev Editor - /Downloads/Daily/main.js
// prompted write
chrome.fileSystem.chooseEntry({type:'saveFile'},function(a){
a.createWriter(function(b){
b.write(new Blob(["Programming fun"],{type:'text/plain'}));
},function(e){trace.innerText = 'error is ' + e;});
});
// unprompted read
chrome.runtime.getPackageDirectoryEntry(function(a){
a.getFile('text.txt',{},function(b){
b.file(function(c){
var d = new FileReader();
d.onloadend = function(){trace.innerText = this.result;};
d.readAsText(c);
});
});
});
// unprompted write - why not?
chrome.runtime.getPackageDirectoryEntry(function(a){
a.getFile('new.txt',{create:true},function(b){
b.createWriter(function(c){
c.write(new Blob(["Miss Manners fan"],{type:'text/plain'}));
},function(e){trace.innerText = 'error is ' + e;});
});
});
}
To be fair, Filesystem API is a big mess of callbacks and it's not unreasonable to get drowned in it.
It's not currently documented, but chrome.runtime.getPackageDirectoryEntry returns a read-only DirectoryEntry, and there is no way to make it writable (it's specifically blacklisted).
You probably don't see an error, because it fails at the getFile stage, for which you don't have an error handler.
Unfortunately, for a Chrome App the only option to write out to a real filesystem is to prompt the user. However, you can retain the entry and ask only once.
If you don't need to write out to the real filesystem but need only internal storage, HTML Filesystem API can help you (yes, it's marked as abandoned, but Chrome maintains it since chrome.fileSystem is built on it).
Extensions additionally have access to chrome.downloads API that enables writing to (but not reading) the Downloads folder.
P.S. What you see in Files app is your "real" local filesystem in ChromeOS + mounted cloud filesystems (e.g. Google Drive)
You can use the basic web Filesystem API. First, add the "unlimitedStorage" permission. Then, copy the packaged files to the sandboxed filesystem, like this:
chrome.runtime.getPackageDirectoryEntry(function(package) {
package.getMetadata(function(metadata) {
webkitRequestFileSystem(PERSISTENT, metadata.size, function(filesystem) {
package.copyTo(filesystem.root)
})
})
})

Debugging Firefox extension - How to see all JS and XUL files contained in the XPI?

I'm trying to debug a Firefox extension, using Firefox 28.0.
I have set up the dev environment as suggested in https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment (actually I just took the lazy way out and installed the DevPrefs extension to set all the necessary about:configs)
I then open Firefox and go into the debugging environment (Tools > Web Developer > Browser Toolbox).
I then go to the Debugger tab.
However, under the Sources pane, under my extension (e.g. chrome://myextension), I only see some of the JS and XUL files that are contained in my extension XPI.
How can I manually "load files" in the debugger, so that I can set a breakpoint and trace the runtime of my extension?
The debugger doesn't have any functionality that would allow loading files "manually", instead it will show you every file that is currently loaded by the JavaScript engine. If you dig into details, this means that whenever the JavaScript engine compiles a new script the debugger is notified and adds the corresponding file to its list. So normally all you need to do is open a page or dialog that uses that script and it will become visible in the debugger. I say "normally" because in my tests this didn't seem to work reliably - there appears to be some bug which makes the debugger miss out some scripts, maybe that's what prompted your question.
Now of course you can consider faking the notification to force the debugger to load a particular file - e.g. if you want to set breakpoints before the file actually loads. I tried it and it is indeed possible, but it requires you to mess with Firefox internals and it relies on a number of implementation details that might change in future Firefox versions. In particular, you need to get the DebuggerServer instance used to communicate with the debugger. While the in-process debugger always uses the same instance which is trivial to get, a new instance is created for each remote debugger. From what I can tell, getting to that instance is only possible with the changes implemented in bug 993029 which means that it will only work with Firefox 32 (currently available from the Firefox Aurora channel) and above.
The problem is that the DebuggerServer instance is being created by the BrowserToolboxProcess class declared in ToolboxProcess.jsm. Before the changes introduced by bug 993029 a BrowserToolboxProcess object would be created and no reference to it kept - meaning that it would be impossible to access it and the corresponding connection after the fact. Starting with Firefox 32 all created BrowserToolboxProcess objects are stored in the processes set and can be enumerated.
This code can be used to fake a Debugger.onNewScript() call that will be forwarded to the remote debugger:
(function()
{
// Iterate over existing processes
const {processes} = Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", null);
for (var process of processes)
{
// Iterate over connections associated with each process
var debuggerServer = process.debuggerServer;
for (var connID in debuggerServer._connections)
{
if (!debuggerServer._connections.hasOwnProperty(connID))
continue;
var conn = debuggerServer._connections[connID];
// Get ChromeDebuggerActor instance for the connection
var chromeDebugger = conn._getOrCreateActor(conn.rootActor._extraActors.chromeDebugger.actorID);
// Fake new script call
chromeDebugger.onNewScript({
url: "chrome://myextension/content/script.js", // CHANGE THAT LINE
source: {text:""},
getChildScripts: () => []
});
}
}
})();
As mentioned above, this code should only work starting with Firefox 32, I tested it on Firefox 33.0a1. You can run it from Scratchpad, make sure to switch environment to "Browser". There is no guarantee whatsoever that it will continue working in future Firefox versions, there are several implementation details used here that can change at any time.

Categories

Resources