I'm trying to create a Firefox add-on that does the following:
I'm using Firefox version 48 for Windows and I can't get it to work. Can someone point out what I'm doing wrong please.
Here is my content script:
// setup message channel between this script and background script
var msgPort = chrome.runtime.connect({name:"msgPort"});
// fires when background script sends a message
msgPort.onMessage.addListener(function(msg) {
console.log(msg.txt);
});
// sends a message to background script when page is clicked
document.body.addEventListener("click", function() {
msgPort.postMessage({txt: "page clicked"});
});
Here is my background script:
var msgPort;
var tmp;
// fires when message port connects to this background script
function connected(prt)
{
msgPort = prt;
msgPort.postMessage({txt: "message channel established"});
msgPort.onMessage.addListener(gotMessage);
}
// fires when content script sends a message
frunction gotMessage(msg)
{
// store the message
chrome.storage.local.set({message : msg.txt});
// read the stored message back again
chrome.storage.local.get("message", function(item){
tmp = item;
});
}
// send the saved message to the content script when the add-on button is clicked
chrome.browserAction.onClicked.addListener(function() {
msgPort.postMessage({txt: "saved message: "+tmp});
});
chrome.runtime.onConnect.addListener(connected);
And here is my manifest:
{
"name": "test",
"manifest_version": 2,
"version": "1.0",
"permissions": ["activeTab","storage"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_title": "Go"
},
"applications": {
"gecko": {
"id": "test@example.com",
"strict_min_version": "48.0a1"
}
}
}
While there may be other issues elsewhere in your code, one issue is that you are not accounting for the fact that chrome.storage.local.set
is asynchronous.
As your code is, your call to chrome.storage.local.get
is being executed immediately after your request to store data via chrome.storage.local.set
without waiting for the data to actually be stored. Thus, the data may not yet be available.
Your gotMessage(msg)
function should look more like:
function gotMessage(msg)
{
// store the message
chrome.storage.local.set({message : msg.txt}, function(){
// read the stored message back again
chrome.storage.local.get("message", function(item){
tmp = item;
//Indicate that the data is available
//Only do this if you desire.
//chrome.browserAction.setTitle({title:'Send data to content script'});
});
});
}
Note: there is also a syntax error in your question where frunction gotMessage(msg)
should be function gotMessage(msg)
.
storage.local
is available to content scripts:
You are currently passing messages back and forth to between your content script and background script in order to set()
and get()
the contents of storage.local
within your background script. I am assuming that you are doing that to test runtime.connect
, browser_action
, etc.. Another possibility is that you are not aware that you can get
and set
with storage.local
from your content script.
There were also multiple other issues. Most of the issues were the result of either the somewhat complex sequence of actions that was needed on the part of the user to get it to work. Some of what was required is due to some peculiarities of WebExtensions/Firefox.:
about:addons
(Ctrl-Shift-A, Cmd-Shift-A on OSX), "remove" the add-on.about:debugging
.runtime.connect()
. This will not connect if there is not first a runtime.onConnect
listener. Thus, as it was the content script had to be loaded after the background script.browser_action
button`Without knowing that you needed to perform exactly this sequence, it is difficult to get it to do what you wanted it to do.
A significant part of the difficulty in figuring out what was going on is that you had a considerable amount of state information which was only contained within the code. No indications were being made to the user as to what state the content script, or the background script were in. In order to help visualize what is happening in each of the scripts, I have added a significant number of calls to console.log
to better illustrate what is happening within your scripts.
I have significantly modified the code:
The output that is now generated in the Browser Console is:
1471884197092 addons.xpi WARN Addon with ID demo-runtime.connect-and-storage.local@example.com already installed, older version will be disabled
content: Content script injected.
content: Making message port available for connection
alert() is not supported in background windows; please use console.log instead.
Open the Browser Console.
background: In background script.
background: listening for a connection
content: page clicked
content: Sending message failed: not yet connected
content: Retrying connection for message: Object { type: "page clicked" }
content: Making message port available for connection
background: Port connected: sending confirmation message: Object { type: "message channel established" }
content: Received message: type: message channel established
content: Sending pending message Object { type: "page clicked" }
content: Sending message Object { type: "page clicked" }
background: Received message: Object { type: "page clicked" }
background: Got data from storage.local.get: Object { message: "page clicked" }
background: Button clicked sending message: Object { type: "saved message", data: "page clicked" }
content: Received message: type: saved message
message: data: page clicked
manifest.json:
{
"name": "Demo runtime.connect and storage.local",
"manifest_version": 2,
"version": "0.1",
"permissions": ["activeTab","storage"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_title": "Data not yet available",
"browser_style": true
},
"applications": {
"gecko": {
"id": "demo-runtime.connect-and-storage.local@example.com",
"strict_min_version": "48.0a1"
}
}
}
background.js:
//* For testing, open the Browser Console
try{
//Alert is not supported in Firefox. This forces the Browser Console open.
//This abuse of a misfeature works in FF49.0b+, not in FF48
alert('Open the Browser Console.');
}catch(e){
//alert throws an error in Firefox versions below 49
console.log('Alert threw an error. Probably Firefox version below 49.');
}
//*
console.log('background: In background script.');
var msgPort;
var dataFromStorage;
// Fires when message port connects to this background script
function connected(prt) {
msgPort = prt;
msgPort.onMessage.addListener(gotMessage); //This should be done first
let message = {type: "message channel established"};
console.log('background: Port connected: sending confirmation message:', message);
msgPort.postMessage(message);
}
//Fires when content script sends a message
//Syntax error this line (misspelled function)
function gotMessage(msg) {
console.log('background: Received message:', msg);
// store the message
chrome.storage.local.set({message : msg.type}, function(){
// read the stored message back again
chrome.storage.local.get("message", function(item){
console.log('background: Got data from storage.local.get:',item);
//You were setting tmp (now dataFromStorage) to the item object, not the
// message key which you requested.
/*
for(x in item){
console.log('background: property of item:',x);
}
//*/
dataFromStorage = item.message;
//Indicate to the user that the data is available
chrome.browserAction.setTitle({title:'Send data to content script'});
});
});
}
// send the saved message to the content script when the add-on button is clicked
chrome.browserAction.onClicked.addListener(function() {
//msgPort not defined unless user has clicked on page
if(msgPort) {
let message = {type: "saved message",data:dataFromStorage};
console.log('background: Button clicked sending message:', message);
msgPort.postMessage(message);
} else {
console.log('background: No message port available (yet).');
}
});
//Be open to establishing a connection. This must be done prior to the
// chrome.runtime.connect elsewhere in your code.
chrome.runtime.onConnect.addListener(connected);
console.log('background: Listening for a connection');
content.js:
console.log('\tcontent: Content script injected.');
var isConnected=false;
var retryConnectionTimerId=-1; //In case we want to cancel it
var retryConnectionCount=0;
var messageBeingRetried=null;
//setup message channel between this script and background script
var msgPort;
function messageListener(msg){
//Using a closure for this function is a bad idea. This should be a named
// function defined at the global scope so we can remove it as a
// listener if the background script sends a message to disconnect.
// You need to be able to disable any active content scripts if the
// add-on is disabled/removed. This is a policy from Mozilla. However,
// for WebExtensions it is not yet possible due to the current lack of the
// runtime.onSuspend event.
if(typeof msg === 'object' && msg.hasOwnProperty('type')){
//Should look specifically for the message indicating connection.
console.log('\tcontent: Received message: type:', msg.type
,(msg.hasOwnProperty('data') ? '\n\t\t\t Message: data:':'')
,(msg.hasOwnProperty('data') ? msg.data : '')
);
if(msg.type === 'disableAddon'){
//Allow for the background script to disable the add-on.
disableThisScript('Received disableAddon message');
}
if(isConnected && msg.type === 'message channel established'){
//We are in a content script that is left over from a previous load
// of this add-on. Or, at least that is the most likely thing
// while testing. This probably needs to change for a real add-on.
// This is here because reloading the temporary add-on does not
// auto-disable any content scripts.
disableThisScript('Received second channel established message');
return;
}//else
isConnected=true; //Any correctly formatted message received indicates connection
//Immediately send a message that was pending (being retried).
// Note: This only immediately sends the message which was most recently attempted
// to send via sendMessage, not all messages which might be waiting in timers.
// Any others will be sent when their timers expire.
sendPendingMessageIfPending();
}else{
console.log('\tcontent: Received message without a "type":', msg);
}
}
function receiveDisconnect(){
//The port was disconnected
disableThisScript('Port disconnected');
isConnected=false;
}
function makePortAvailableForConnection(){
console.log('\tcontent: Making message port available for connection');
if(msgPort && typeof msgPort.disconnect === 'function'){
//Call disconnect(), if we have already tried to have a connection
msgPort.disconnect();
}
//Try to make a connection. Only works if ocConnect listener
// is already established.
msgPort = chrome.runtime.connect({name:"msgPort"});
//Fires when background script sends a message
msgPort.onMessage.addListener(messageListener);
msgPort.onDisconnect.addListener(receiveDisconnect);
//Can not use runtime.onConnect to detect that we are connected.
// It only fires if some other script is trying to connect
// (using chrome.runtime.connect or chrome.tabs.connect)
// to this script (or generally). It does not fire when the connection
// is initiated by this script.
chrome.runtime.onConnect.addListener(portConnected); //Does not fire
}
function portConnected(){
//This event does not fire when the connection is initiated,
// chrome.runtime.connect() from this script.
// It is left in this code as an example and to demonstrate that the event does
// not fire.
console.log('\tcontent: Received onConnect event');
isConnected=true;
}
// sends a message to background script when page is clicked
function sendClickMessage() {
console.log('\tcontent: Page clicked');
sendMessage({type: "page clicked"});
chrome.storage.local.get("message", function(item){
console.log('content: Got data from storage.local.get:',item);
});
}
function clearPendingMessage(){
window.clearTimeout(retryConnectionTimerId);
messageBeingRetried=null;
}
function sendPendingMessageIfPending() {
//Pending messages should really be implemented as a queue with each message
// being retried X times and then sent once a connection is made. Right now
// this works for a single message. Any other messages which were pending
// are only pending for the retry period and then they are forgotten.
if(messageBeingRetried !== null && retryConnectionTimerId){
let message = messageBeingRetried;
clearPendingMessage();
console.log('\tcontent: Going to send pending message', message);
sendMessage(message);
}
}
function retryingMessage(message) {
retryConnectionTimerId=-1;
messageBeingRetried=null;
sendMessage(message);
}
function sendMessage(message) {
if(isConnected){
try{
console.log('\tcontent: Sending message', message);
msgPort.postMessage(message);
retryConnectionCount=0;
}catch(e){
if(e.message.indexOf('disconnected port') > -1){
console.log('\tcontent: Sending message failed: disconnected port');
if(isConnected){
console.log('\tcontent: Had connection, but lost it.'
+ ' Likely add-on reloaded. So, disable.');
disableThisScript('Add-on likely reloaded.');
}else{
retryConnection(message);
}
}else{
console.log('\tcontent: Sending message failed: Unknown error', e);
}
}
}else{
console.log('\tcontent: Sending message failed: not yet connected');
retryConnection(message);
}
}
function retryConnection(message){
if(retryConnectionCount>=5){
//Limit the number of times we retry the connection.
// If the connection is not made by now, it probably won't be
// made at all. Don't fill up the console with a lot of
// messages that might make it harder to see what is happening.
retryConnectionCount=0; //Allow more retries upon another click event.
//The current message is forgotten. It is now just discarded.
return;
}
console.log('\tcontent: Retrying connection for message:', message);
makePortAvailableForConnection();
//Try sending the message after a timeout.
// This will result in the repeated attempts to
// connect and send the message.
messageBeingRetried=message;
retryConnectionTimerId = window.setTimeout(retryingMessage,500,message);
retryConnectionCount++;
}
function disableThisScript(reason){
console.log('\tcontent: Disable the content script:', reason);
//Gracefully disable everything previously set up.
msgPort.onMessage.removeListener(messageListener);
msgPort.onDisconnect.removeListener(receiveDisconnect);
try{
msgPort.disconnect();
}catch(e){
//most likely the port was already disconnected
}
chrome.runtime.onConnect.removeListener(portConnected);
document.body.removeEventListener("click", sendClickMessage);
isConnected=false;
}
//Making the connection available will silently fail if there is not already a
// onConnect listener in the background script. In the case of a "temporary"
// add-on upon load or reload, the content script is run first and
// no connection is made.
makePortAvailableForConnection();
document.body.addEventListener("click", sendClickMessage);
NOTE:
This works for a demo, or learning, but won't work well for use in a production extension. There is nothing in the code that accounts for having more than one tab.
You are sending messages to a content script based on clicking on the browser_action
button. This means that you need to be able to explicitly send the message from the background script only to the content script that is in the tab being displayed in the active window at the time the user clicks the browser_action
button. For example, the user might be in one page, click on the content, then switch to another tab, and click on the browser_action
button. In this case, the message should be sent to the currently active tab, not the one which was switched away from (which made the connection).
While you could track the tab which was active when you received the connection or click message (which would work because the connection is actually made, or a message sent, based on a click event (which is assumed to be user input)), it might be better to use tabs.connect()
which allows you to establish a connection only with a specific tab. Knowing which runtime.Port
corresponds to each tab will allow you to be sure you are sending messages only to the tab which was active at the time the browser_action
button was clicked. You would need to keep an array, or object, that contained the connected ports indexed by tab ID.