In my chrome extension with manifest V2 I was using chrome.webRequest.onBeforeRequest to get all requests of current tab. Here is
const dataSet = {};
chrome.webRequest.onBeforeRequest.addListener(function (details) {
if (details && details.url && details.type == "image") {
if (!dataSet[tabId]) {
dataSet[tabId] = new Set([]);
}
const currentSet = dataSet[tabId];
currentSet.add(details.url);
}
}, {
urls: ["<all_urls>"]
});
I'm trying same code in manifest version 3 but event didn't triggers. Also I've tried this workaround but it still didn't works.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
// this event is not being triggered
chrome.webRequest.onBeforeRequest.addListener(function(details){
},{urls: ["<all_urls>"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
Also tried to use webNavigation.onHistoryStateUpdated but still onBeforeRequest didn't triggers
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up', details);
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
console.log(details);
},
{
urls: ['<all_urls>'],
},
);
});
Console output of background page
I'm trying to develop a simple chrome extension. There is a pageAction's default icon that should appear on the pages with a specific URL (http://www.example.com/*).
There is a two file
manifest.json
{
"manifest_version": 2,
"name": "name",
"description": "description",
"version": "1.0",
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"page_action": {
"default_icon" : "images/icons/19.png"
},
"permissions": [
"declarativeContent"
]
}
background.js
chrome.runtime.onInstalled.addListener(function () {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
chrome.declarativeContent.onPageChanged.addRules([
{
// rule1
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {urlPrefix : 'http://www.example.com/'}
})
],
actions : [
new chrome.declarativeContent.ShowPageAction()
]
},
{
// rule2
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [
new chrome.declarativeContent.SetIcon({
path : {"19" : "images/icons/green.png"}
})
]
}
]);
});
});
rule1 should show pageAction's icon and rule2 should change icon to green version on the pages with URL that looks like http://www.example.com/?q1=green
But during installation of extension things come to:
Error in response to events.removeRules: Error: Invalid value for argument 1. Property '.0': Value does not match any valid type choices.
I dug deeply into this error, and it seems like the documentation does not reflect well the fact that using path parameter is not implemented. This is certainly a bug, tracked here.
For now, to fix this you need to load the image and convert it to ImageData format before calling SetIcon.
// Takes a local path to intended 19x19 icon
// and passes a correct SetIcon action to the callback
function createSetIconAction(path, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image = new Image();
image.onload = function() {
ctx.drawImage(image,0,0,19,19);
var imageData = ctx.getImageData(0,0,19,19);
var action = new chrome.declarativeContent.SetIcon({imageData: imageData});
callback(action);
}
image.src = chrome.runtime.getURL(path);
}
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
createSetIconAction("images/icons/green.png", function(setIconAction) {
chrome.declarativeContent.onPageChanged.addRules([
/* rule1, */
{
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [ setIconAction ]
}
]);
});
});
If needed, this can be generalized to support high-DPI icon (19 + 38):
function createSetIconAction(path19, path38, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image19 = new Image();
image19.onload = function() {
ctx.drawImage(image19,0,0,19,19); // fixed
var imageData19 = ctx.getImageData(0,0,19,19);
var image38 = new Image();
image38.onload = function() {
ctx.drawImage(image38,0,0,38,38);
var imageData38 = ctx.getImageData(0,0,38,38);
var action = new chrome.declarativeContent.SetIcon({
imageData: {19: imageData19, 38: imageData38}
});
callback(action);
}
image38.src = chrome.runtime.getURL(path38);
}
image19.src = chrome.runtime.getURL(path19);
}
In fact, you can use new chrome.declarativeContent.SetIcon({ path:'yourPath.png' }),
No need to specify size path: {"19": "images/icons/green.png"}, its default value is: 16
Use declarativeContent.SetIcon need to pay attention to a problem, it is actually a bug.
Actual use of path will eventually be automatically converted to ImageData.
see screenshot:
The root cause of the error of declarativeContent.SetIcon is: it is an asynchronous API, but at the same time it has no asynchronous callback. The only thing you can do is wait.
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
console.log(action.imageData); // => undefined
see screenshot:
// invalid
new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' }, action => console.log(action));
It takes a while to wait:
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
setTimeout(() => {
console.log(action.imageData); // {16: ArrayBuffer(1060)}
}, 5);
see screenshot:
When you understand the reason for the error of SetIcon, the problem will be solved well.
You only need to put the operation of addRules in the event.
onInstalled event
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
});
});
pageAction.onClicked
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.pageAction.onClicked.addListener(() => {
if (condition) {
chrome.declarativeContent.onPageChanged.removeRules(['hideAction']);
} else {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
}
});
There are some related information:
SetIcon source code
declarativeContent.SetIcon = function (parameters) {
// TODO(devlin): This is very, very wrong. setIcon() is potentially
// asynchronous (in the case of a path being specified), which means this
// becomes an "asynchronous constructor". Errors can be thrown *after* the
// `new declarativeContent.SetIcon(...)` call, and in the async cases,
// this wouldn't work when we immediately add the action via an API call
// (e.g.,
// chrome.declarativeContent.onPageChange.addRules(
// [{conditions: ..., actions: [ new SetIcon(...) ]}]);
// ). Some of this is tracked in http://crbug.com/415315.
setIcon(
parameters,
$Function.bind(function (data) {
// Fake calling the original function as a constructor.
$Object.setPrototypeOf(this, nativeSetIcon.prototype);
$Function.apply(nativeSetIcon, this, [data]);
}, this)
);
};
Discussion of related issues:
http://crbug.com/415315
No solution
As the guys before me mentioned, this is a bug. There are no solutions, only workarounds.
Workarounds
#1: Draw icon using canvas
As Xan described in his answer already.
#2 Wait for icon load (timeout hack)
Thanks to weiya-ou's answer I realized that I can just wait for the async icon data transformation to finish.
// Make your handler `async`
chrome.runtime.onInstalled.addListener(async () => {
const action = await new chrome.declarativeContent.SetIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
})
// THE WAIT STARTS
// Wait max. 10 loops
for (let i = 0; i < 10; i++) {
// Create a promise
const checkAvailability = new Promise((resolve) => {
// Resolve promise after 100ms
setTimeout(() => resolve(!!action.imageData), 100)
})
// Wait for the promise resolution
const isAvailable = await checkAvailability
// When image available, we are done here
if (isAvailable) break
}
// THE WAIT ENDS
const condition = new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: 'my.page.net' },
})
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [condition],
actions: [action],
},
]);
});
});
#3 Use chrome.tabs
You would need the tabs permission (as said here).
chrome.tabs.onUpdated.addListener((tabId, { status }, { url }) => {
// Only check when URL is resolved
if (status !== 'complete') return
// What is our target page?
const isOurPage = url?.match(/my\.page\.net/)
if (isOurPage) {
// Show active icon
chrome.pageAction.setIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
tabId,
})
} else {
// Show inactive icon
chrome.pageAction.setIcon({
path: {
19: 'images/19-inactive.png',
38: 'images/38-inactive.png',
},
tabId,
})
}
})
I need to check when the extension is installed and change my React state accordingly.
I use chrome.runtime.onInstalled on my background.js where I sendMessage to my react code - which is the content script of my extension.
background.js
async function getCurrentTab() {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
return tab;
}
chrome.runtime.onInstalled.addListener((details) => {
if (details?.reason === 'install') {
console.log('installed backgroundjs')
const tab = await getCurrentTab()
chrome.tabs.sendMessage(tab.id, { target: 'onInstall' })
openNewTab()
}
})
In my react Component - Dashboard.js
useEffect(() => {
if (extensionApiObject?.runtime) {
chrome.runtime.sendMessage({ target: 'background', message: 'check_installation', })
console.log('extension')
chrome.runtime.onMessage.addListener(handleMessage)
}
return () => {
if (extensionApiObject?.runtime) {
chrome.runtime.onMessage.removeListener(handleMessage)
}
}
})
function handleMessage(msg) {
console.log('handle messag func', msg)
if (msg.target === 'onInstall') {
console.log('extension on Install')
setShowWelcomeMessage(true)
}
}
What confuse me is that I already have a similar implementation for a different message that works without problem but in there I listen to chrome.runtime.onMessage() not chrome.runtime.onInstalled()
I wonder if I misunderstand how onInstalled method work and I cannot sendMessage from it?
UPDATE:
I change my code as suggested by #wOxxOm, I used chrome.tabs.sendMessage but still no luck.
chrome.runtime.onInstalled doesn't take as argument req, sender, sendResponse like other listener and I wonder if that means it will not be able to send a message from there :/
After #wOxxOm suggestions, I ended up removing the sendMessage solution and simply add an extra parameter to the url whenever I open a new tab after installation:
background.js
function openNewTab(param) {
chrome.tabs.create({
url: param ? `chrome://newtab?${param}` : 'chrome://newtab',
})
}
chrome.runtime.onInstalled.addListener((details) => {
if (details?.reason === 'install') {
chrome.tabs.sendMessage(tab.id, { target: 'onInstall' })
openNewTab('installed')
}
})
on my web app I just need to check the param and I can decide which UI to show
I have a content script in a Chrome Extension that's passing messages. Every so often, when the content script calls
chrome.runtime.sendMessage({
message: 'hello',
});
it throws an error:
Uncaught Error: Extension context invalidated.
What does this error mean? I couldn't find any documentation on it.
It doesn't happen consistently. In fact, it's hard to reproduce. Seems to happen if I just leave the page open for a while in the background.
Another clue: I've written many Chrome Extensions with content scripts that pass messages and I haven't seen this error before. The main difference is that this content script is injected by the background page using
chrome.tabs.executeScript({
file: 'contentScript.js',
});
Does using executeScript instead of the manifest file somehow change the lifecycle of the content script?
This is certainly related to the message listener being lost in the middle of the connection between content and background scripts.
I've been using this approach in my extensions, so that I have a single module that I can use in both background and content scripts.
messenger.js
const context = (typeof browser.runtime.getBackgroundPage !== 'function') ? 'content' : 'background'
chrome.runtime.onConnect.addListener(function (port) {
port.onMessage.addListener(function (request) {
try {
const object = window.myGlobalModule[request.class]
object[request.action].apply(module, request.data)
} catch () {
console.error(error)
}
})
})
export function postMessage (request) {
if (context === 'content') {
const port = chrome.runtime.connect()
port.postMessage(request)
}
if (context === 'background') {
if (request.allTabs) {
chrome.tabs.query({}, (tabs) => {
for (let i = 0; i < tabs.length; ++i) {
const port = chrome.tabs.connect(tabs[i].id)
port.postMessage(request)
}
})
} else if (request.tabId) {
const port = chrome.tabs.connect(request.tabId)
port.postMessage(request)
} else if (request.tabDomain) {
const url = `*://*.${request.tabDomain}/*`
chrome.tabs.query({ url }, (tabs) => {
tabs.forEach((tab) => {
const port = chrome.tabs.connect(tab.id)
port.postMessage(request)
})
})
} else {
query({ active: true, currentWindow: true }, (tabs) => {
const port = chrome.tabs.connect(tabs[0].id)
port.postMessage(request)
})
}
}
}
export default { postMessage }
Now you'll just need to import this module in both content and background script. If you want to send a message, just do:
messenger.postMessage({
class: 'someClassInMyGlobalModuçe',
action: 'someMethodOfThatClass',
data: [] // any data type you want to send
})
You can specify if you want to send to allTabs: true, a specific domain tabDomain: 'google.com' or a single tab tabId: 12.
I'm using the VM2 package to run user code. I'm trying to intercept console output and have set the NodeVM object's console property to 'redirect':
// Create a new sandbox VM for this request
const vm = new NodeVM( {
console: 'redirect',
timeout: 30000,
sandbox: { request, state, response },
require: {
external: true
}
});
According to the documentation that redirects console output to 'events'. I'm new to NodeJS, how do I hook into those events to capture the console.log messages executed inside the Sandbox?
After digging through the source code, I found this file where the event emit is occuring:
sandbox.js
if (vm.options.console === 'inherit') {
global.console = Contextify.readonly(host.console);
} else if (vm.options.console === 'redirect') {
global.console = {
log(...args) {
vm.emit('console.log', ...Decontextify.arguments(args));
return null;
},
info(...args) {
vm.emit('console.info', ...Decontextify.arguments(args));
return null;
},
warn(...args) {
vm.emit('console.warn', ...Decontextify.arguments(args));
return null;
},
error(...args) {
vm.emit('console.error', ...Decontextify.arguments(args));
return null;
},
dir(...args) {
vm.emit('console.dir', ...Decontextify.arguments(args));
return null;
},
time: () => {},
timeEnd: () => {},
trace(...args) {
vm.emit('console.trace', ...Decontextify.arguments(args));
return null;
}
};
}
All you need to do to listen to these events is to bind an event listener on the vm you've created:
// Create a new sandbox VM for this request
const vm = new NodeVM( {
console: 'redirect',
require: {
external: ['request']
}
});
vm.on('console.log', (data) => {
console.log(`VM stdout: ${data}`);
});
Likewise, you can bind to console.log, console.info, console.warn, console.error, console.dir, and console.trace. Hopefully this will save someone else some time.