Chrome extension: create new window in depends on window presence - javascript

I create a new window in my Chrome extension like this:
chrome.windows.create({
url: 'https://example.com',
focused: false,
state: "minimized"
}, function(hiddenWindow) {
var code = "console.log('Some JS code goes here');"
chrome.tabs.onUpdated.addListener(function(tabId, info) {
if (info.status === 'complete') {
chrome.tabs.executeScript(hiddenWindow.tabs[0].id, {
code: code
},
function(results) {
console.log(results);
});
}
});
});
It is possible somehow to do this one:
First time we create a window with one tab inside (like in my code above)
Then each time we check if this window is not closed by user
If the window still exists, then open a new tab in this window and close the previous tab.
If this window no longer exists, then do it all over again starting from #1
Will be grateful for any help and ideas!

Store the id of the window in a variable (global, for example) and use chrome.windows.get - when the window is closed the API will return an error in chrome.runtime.lastError. Also, instead of closing the previous tab it seems simpler to navigate the existing tab to a new URL.
Now, the above scheme would require us to use an elaborate cascade of callbacks at worst or Promises at best, but since it's 2019 let's use the modern async/await syntax instead with the help of Mozilla WebExtension polyfill.
let wndId;
const wndOptions = {
focused: false,
state: 'minimized',
};
const code = `(${() => {
console.log('Some JS code goes here');
}})()`;
async function openMinimized(url) {
const w =
wndId &&
await browser.windows.get(wndId, {populate: true}).catch(() => {}) ||
await browser.windows.create({url, ...wndOptions});
wndId = w.id;
const [wTab] = w.tabs;
if (wTab.url !== url) browser.tabs.update(wTab.id, {url});
return new Promise(resolve => {
browser.tabs.onUpdated.addListener(async function onUpdated(tabId, info) {
if (tabId === wTab.id && info.status === 'complete') {
browser.tabs.onUpdated.removeListener(onUpdated);
resolve(browser.tabs.executeScript(tabId, {code}));
}
});
});
}
Usage:
openMinimized('https://example.com').then(results => {
console.log(results);
});
Notes:
onUpdated listener checks tabId to process only this tab
onUpdated listener unregisters itself
code can be written as normal JS with syntax highlight inside an IIFE in a template string
How to use the polyfill: download browser-polyfill.min.js from its official repo on unpkg, save in your extension directory, and load it just like any other script in your extension, for example, as a background script in manifest.json:
"background": {
"scripts": ["browser-polyfill.min.js", "background.js"]
}

Related

Inject content script only once in a webpage

I am trying to inject content script on context menu click in an extension manifest version 3. I need to check if it is already injected or not. If it is not injected , inject the content script. This condition has to be satisfied. Can anyone help me with this?
We can use
ALREADY_INJECTED_FLAG
but this can be checked only in the content script, so this approach will not work as expected.
payload.js(content script)
function extract() {
htmlInnerText = document.documentElement.innerText;
url_exp = /[-a-zA-Z0-9#:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9#:%_\+.~#?&//=]*)?/gi;
regex = new RegExp(url_exp)
list_url = htmlInnerText.match(url_exp)
ip_exp = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
list_ip = htmlInnerText.match(ip_exp)
hash_exp = /\b[A-Fa-f0-9]{32}\b|\b[A-Fa-f0-9]{40}\b|\b[A-Fa-f0-9]{64}\b/g
list_hash = htmlInnerText.match(hash_exp)
chrome.storage.local.set({ list_url: list_url, list_ip: list_ip, list_hash: list_hash });
}
chrome.runtime.sendMessage( extract());
background.js
genericOnClick = async () => {
// Inject the payload.js script into the current tab after the backdround has loaded
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["payload.js"]
},() => chrome.runtime.lastError);
});
// Listen to messages from the payload.js script and create output.
chrome.runtime.onMessage.addListener(async (message) => {
chrome.storage.local.get("list_url", function (data) {
if (typeof data.list_url != "undefined") {
urls = data.list_url
}
});
chrome.storage.local.get("list_ip", function (data) {
if (typeof data.list_ip != "undefined") {
ips = data.list_ip
}
});
chrome.storage.local.get("list_hash", function (data) {
if (typeof data.list_hash != "undefined") {
hashes = data.list_hash;
}
});
if ( hashes.length>0 || urls.length>0 || ips.length>0 ){
chrome.windows.create({url: "output.html", type: "popup", height:1000, width:1000});
}
});
}
on my first context menu click I get the output html once. Second time
I click, I get the output html twice likewise.
This behavior is caused by a combination of two factors.
First factor
You're calling chrome.runtime.onMessage.addListener() inside genericOnClick(). So every time the user clicks the context menu item, the code adds a new onMessage listener. That wouldn't be a problem if you passed a named function to chrome.runtime.onMessage.addListener(), because a named function can only be registered once for an event.
function on_message(message, sender, sendResponse) {
console.log("bg.on_message");
sendResponse("from bg");
}
chrome.runtime.onMessage.addListener(on_message);
Second factor
But you're not registering a named function as the onMessage handler. You're registering an anonymous function. Every click on the context menu item creates and registers a new anonymous function. So after the Nth click on the context menu item, there will be N different onMessage handlers, and each one will open a new window.
Solution
Define the onMessage handler as a named function, as shown above.
Call chrome.runtime.onMessage.addListener() outside of a function.
You don't have to do both 1 and 2. Doing either will solve your problem. But I recommend doing both, because it's cleaner.

How to toggle switch chrome extension

I am new to developing chrome extensions.
I am trying to build a simple extension and I want that extension to be toggle between on and off states via the icon.
Here is the code I am using so far for achieving this:
(background.js)
var enabled = 1;
chrome.action.onClicked.addListener(function () {
console.log(enabled);
if (enabled == 0) {
enabled = 1;
chrome.action.setIcon({
path: "images/on.png"
});
} else {
enabled = 0;
chrome.action.setIcon({
path: "images/off.png"
});
}
});
And then in the same file, I am doing the stuff I want to do.
This works so far but the problem is, yes the thing works with an if statement:
if(enabled == 1){
do this...}
But after a while because of a reason that I couldn't understand (I've done console log debugs with the "enabled" variable, it works fine), the extension keeps working since its in the disabled state and the icon shows its disabled...
The background script is terminated after 30 seconds since the last event such as onClicked, so you can't rely on global variables as they will be reset when the background script starts again for a new event after it was terminated.
One solution is to change the hash part of the popup's file name, meaning that the same popup file will be shown, however its location.hash will be different:
background script:
chrome.action.onClicked.addListener(async () => {
const popup = await chrome.action.getPopup({});
const enabled = popup.includes('#off'); // new value of `enabled`
const newPopup = popup.split('#')[0] + (enabled ? '' : '#off');
chrome.action.setIcon({path: `images/${enabled ? 'on' : 'off'}.png`});
await chrome.action.setPopup({popup: newPopup});
});
popup script:
const enabled = !location.hash.includes('#off');
//document.body.prepend('Enabled: ' + enabled);
Another solution is to store the state in chrome.storage.local:
chrome.action.onClicked.addListener(async () => {
const enabled = !(await chrome.storage.local.get('enabled')).enabled;
const path = `images/${enabled ? 'on' : 'off'}.png`;
chrome.action.setIcon({path});
await chrome.storage.local.set({enabled});
});
manifest.json:
"permissions": ["storage"]

JavaScript callback / async problem? Background script of Chrome Extension stops function before finishing

I am trying to create a small crawler as a chrome extension. How it works is:
Open new window or tab.
Perform a search for Google / Google News / YouTube with given keywords.
Store information of the results in a small database
I first created and tested the functions with a popup.html. There it works perfectly.You click on a button and all pages are visited and the results are stored in a database. But I want to start the program without clicking anything first. That's why I migrated it to background.js. There it also works, but only if the Service Worker / DevTool console is open. Only then it runs completely.
I would be grateful for any helpful answer.
const keywords = [
"Keyword1",
"Keyword2",
// ...
"Keyword13"
];
chrome.runtime.onStartup.addListener(() => {
chrome.tabs.onUpdated.addListener(loadingWindow);
openWindow();
});
// Opens new Window or Tab with the correct URL
function openWindow() {
chrome.tabs.onUpdated.addListener(loadingWindow);
if (runs == 0) {
chrome.windows.create({ url: getUrl(keywords[runs]), type: "normal" }, newWindow => {
window_id = newWindow.id;
});
} else {
chrome.tabs.update(tab_id, { url: getUrl(keywords[runs]) });
}
}
// Wait to load the new tab
function loadingWindow(tabId, changeInfo, tab) {
if (changeInfo.status === 'complete' && tab.status == 'complete' && tab.windowId == window_id) {
tab_id = tabId;
console.log(tab.windowId);
chrome.tabs.onUpdated.removeListener(loadingWindow);
chrome.tabs.sendMessage(tab.id, { text: source }, doStuffWithDom);
}
};
// Get information from content script -> payload and then send to database
function doStuffWithDom(domContent) {
let payload = {... }
var data = new FormData();
data.append("json", JSON.stringify(payload));
fetch(".../store.php", { method: "POST", body: data });
crawlDone();
}
// open new window / tab or close the open window
function crawlDone() {
runs++;
if (runs < keywords.length) {
openWindow();
} else if (runs == keywords.length) {
chrome.windows.remove(window_id);
}
};
I switched to Manifest version 2. Then I could include the Background.js via the Background.html, which also runs to the end.

JavaScript open a tab and track when it is closed

I open a tab from JavaScript and try to track when it is closed. But none of the events is fired. I only find examples with onbeforeunload referencing the current window, not other window-objects.
window.addEventListener('DOMContentLoaded', () => {
document.querySelector('#youtube-open').addEventListener('click', () => {
document.yTT = window.open('https://youtube.com', '_blank');
document.yTT.addEventListener('close', () => {
console.log('onclose fired');
});
document.yTT.addEventListener('beforeunload', () => {
console.log('onbeforeunload fired');
});
document.yTT.addEventListener('unload', () => {
console.log('onunload fired');
});
});
});
There are no errors in the JS console or something. It just doesn't work. Any ideas why?
you can not access the other window instances of a browser. A way to bypass that is to use the local storage(which can be accessed from the events you listed above), to store some cross-tab data and a polling mechanism(in your main tab) to get the states.
Yes it is possible to track when the window is closed just not that way.
Your document.yTT variable holds the WindowProxy object returned by window.open(). That object will not emit the events you are trying to listen to unfortunately. It will however hold a property that says if the window has been closed.
You can do this if you want to call a function when the window is closed :
window.addEventListener('DOMContentLoaded', () => {
document.querySelector('#youtube-open').addEventListener('click', () => {
document.yTT = window.open('https://youtube.com', '_blank');
document.yTTInterval = setInterval(() => {
if (document.yTT.closed) {
clearInterval(document.yTTInterval);
executeSomeFunction();
}
}, 1); // or whatever interval works for you, the longer the less consuming
});
});

How do I open a url from <a> on default OS browser in Electron app? [duplicate]

I'm using the BrowserWindow to display an app and I would like to force the external links to be opened in the default browser. Is that even possible or I have to approach this differently?
I came up with this, after checking the solution from the previous answer.
mainWindow.webContents.on('new-window', function(e, url) {
e.preventDefault();
require('electron').shell.openExternal(url);
});
According to the electron spec, new-window is fired when external links are clicked.
NOTE: Requires that you use target="_blank" on your anchor tags.
new-window is now deprecated in favor of setWindowOpenHandler in Electron 12 (see https://github.com/electron/electron/pull/24517).
So a more up to date answer would be:
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
Improved from the accepted answer ;
the link must be target="_blank" ;
add in background.js(or anywhere you created your window) :
window.webContents.on('new-window', function(e, url) {
// make sure local urls stay in electron perimeter
if('file://' === url.substr(0, 'file://'.length)) {
return;
}
// and open every other protocols on the browser
e.preventDefault();
shell.openExternal(url);
});
Note : To ensure this behavior across all application windows, this code should be run after each window creation.
If you're not using target="_blank" in your anchor elements, this might work for you:
const shell = require('electron').shell;
$(document).on('click', 'a[href^="http"]', function(event) {
event.preventDefault();
shell.openExternal(this.href);
});
I haven't tested this but I assume this is should work:
1) Get WebContents of the your BrowserWindow
var wc = browserWindow.webContents;
2) Register for will-navigate of WebContent and intercept navigation/link clicks:
wc.on('will-navigate', function(e, url) {
/* If url isn't the actual page */
if(url != wc.getURL()) {
e.preventDefault();
openBrowser(url);
}
}
3) Implement openBrowser using child_process. An example for Linux desktops:
var openBrowser(url) {
require('child_process').exec('xdg-open ' + url);
}
let me know if this works for you!
For anybody coming by.
My use case:
I was using SimpleMDE in my app and it's preview mode was opening links in the same window. I wanted all links to open in the default OS browser. I put this snippet, based on the other answers, inside my main.js file. It calls it after it creates the new BrowserWindow instance. My instance is called mainWindow
let wc = mainWindow.webContents
wc.on('will-navigate', function (e, url) {
if (url != wc.getURL()) {
e.preventDefault()
electron.shell.openExternal(url)
}
})
Check whether the requested url is an external link. If yes then use shell.openExternal.
mainWindow.webContents.on('will-navigate', function(e, reqUrl) {
let getHost = url=>require('url').parse(url).host;
let reqHost = getHost(reqUrl);
let isExternal = reqHost && reqHost != getHost(wc.getURL());
if(isExternal) {
e.preventDefault();
electron.shell.openExternal(reqUrl);
}
}
Put this in renderer side js file. It'll open http, https links in user's default browser.
No JQuery attached! no target="_blank" required!
let shell = require('electron').shell
document.addEventListener('click', function (event) {
if (event.target.tagName === 'A' && event.target.href.startsWith('http')) {
event.preventDefault()
shell.openExternal(event.target.href)
}
})
For Electron 5, this is what worked for me:
In main.js (where you create your browser window), include 'shell' in your main require statement (usually at the top of the file), e.g.:
// Modules to control application life and create native browser window
const {
BrowserWindow,
shell
} = require('electron');
Inside the createWindow() function, after mainWindow = new BrowserWindow({ ... }), add these lines:
mainWindow.webContents.on('new-window', function(e, url) {
e.preventDefault();
shell.openExternal(url);
});
I solved the problem by the following step
Add shell on const {app, BrowserWindow} = require('electron')
const {app, BrowserWindow, shell} = require('electron')
Set nativeWindowOpen is true
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1350,
height: 880,
webPreferences: {
nativeWindowOpen: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, './img/icon.icns')
})
Add the following listener code
mainWindow.webContents.on('will-navigate', function(e, reqUrl) {
let getHost = url=>require('url').parse(url).host;
let reqHost = getHost(reqUrl);
let isExternal = reqHost && reqHost !== getHost(wc.getURL());
if(isExternal) {
e.preventDefault();
shell.openExternal(reqUrl, {});
}
})
reference https://stackoverflow.com/a/42570770/7458156 by cuixiping
I tend to use these lines in external .js script:
let ele = document.createElement("a");
let url = "https://google.com";
ele.setAttribute("href", url);
ele.setAttribute("onclick", "require('electron').shell.openExternal('" + url + "')");

Categories

Resources