In an add-on for Firefox, I'm trying to inject code from a background script into a tab and then pass a message to it. Unfortunately, the content script seems to add the listener only after the message has already been sent, resulting in in error. What am I missing? Here is my sample code:
manifest.json:
{
"description": "Test background to content message passing",
"manifest_version": 2,
"name": "Background content message passing",
"version": "0.1.0",
"default_locale": "en",
"applications": {
"gecko": {
"id": "bcm#example.com",
"strict_min_version": "51.0"
}
},
"permissions": [
"contextMenus",
"<all_urls>"
],
"background": {
"scripts": ["background.js"]
}
}
background.js:
"use strict";
const {contextMenus, i18n, runtime, tabs} = browser;
contextMenus.onClicked.addListener(function(info, tab) {
if (info.menuItemId == "bgd-cnt-msg") {
tabs.executeScript(tab.id, {
file: "/content.js",
})
.then(runtime.sendMessage({"result": 42}))
.then(console.log("Debug: runtime message sent"))
.catch(console.error.bind(console));
}
});
contextMenus.create({
id: "bgd-cnt-msg",
title: "Test message passing",
contexts: ["all"],
documentUrlPatterns: ["<all_urls>"]
});
content.js
"use strict";
console.log("Debug: executing content script");
browser.runtime.onMessage.addListener(function (message) {
console.log("Debug: received message %O", message);
});
console.log("Debug: added listener");
The result of selecting the the context menu entry is
Debug: runtime message sent background.js:11:15
Debug: executing content script content.js:3
Debug: added listener content.js:9
Error: Could not establish connection. Receiving end does not exist. undefined
I.e., the context scripts executes after the message to the tab is sent. How can I add the listener before sending the message?
As suggested by #Thắng, I changed my code to use tabs.sendMessage instead of runtime.sendMessage:
contextMenus.onClicked.addListener(function(info, tab) {
if (info.menuItemId == "bgd-cnt-msg") {
tabs.executeScript(tab.id, {
file: "/content.js",
})
.then(tabs.sendMessage(tab.id, {"result": 42}))
.then(console.log("Debug: runtime message sent"))
.catch(console.error.bind(console));
}
});
Now the error is reported a little earlier:
Debug: runtime message sent background.js:11:15
Error: Could not establish connection. Receiving end does not exist. undefined
Debug: executing content script content.js:3
Debug: added listener content.js:9
Thanks to #Thắng, who provided a working solution, I fixed my code to not only use tabs.sendMessage but also pass functions for the callbacks:
contextMenus.onClicked.addListener(function(info, tab) {
if (info.menuItemId == "bgd-cnt-msg") {
tabs.executeScript(tab.id, {
file: "/content.js",
})
.then(function () { tabs.sendMessage(tab.id, {"result": 42}) })
.then(function () { console.log("Debug: runtime message sent") })
.catch(console.error.bind(console));
}
});
With an additional fix in content.js
browser.runtime.onMessage.addListener(function (message) {
console.log("Debug: result is " + message.result);
});
I now get
Debug: executing content script content.js:3
Debug: added listener content.js:9
Debug: runtime message sent background.js:11:15
Debug: result is 42 content.js:6
In background script, you need to let it know it should send the message to which tab, so don't use runtime.sendMessage for this.
var sending = chrome.tabs.sendMessage(
tabId, // integer
message, // any
options // optional object
)
See more here (for webExtensions but also compatible with Chrome): https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/sendMessage
Your fully working extension here (you may need to change all the browser.* to chrome.*):
https://drive.google.com/file/d/1KGf8tCM1grhhiC9XcHOjsrbBsIZGff3e/view?usp=sharing
Related
All of the information I can find on this is pretty old. Like the title says I am trying to make a global variable in one script and access it from another. The purpose of the extension is to search for a class named "page-title" and then return the innerHTML of that HTML element. Once I get the code working I will specify the URL I want the extension to run on so it's not constantly running.
After a couple iterations trying to accomplish this in different ways I followed the method explained in this answer but my needs have different requirements and I am receiving the error "Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist." tied to the popup.html.
I tried the Unchecked runtime error solution found here but it's been awhile (~ 7 years) since I've dived into any coding and I'm not sure I implemented it correctly.
I've also tried to pass the value between JS documents is the HTML injection method, but without overriding security defaults in the manifest that doesn't really work. It also seemed super bootstrappy and I wanted to pass the information in a more conventional way. I tried creating a global variable by simply declaring the variable outside of a function/class/if statement and loading that .js file first, but that was unsuccessful as well.
Manifest
"name": "P.P. to Sharepoint",
"version": "1.0.0",
"description": "Open P.P. client folder in sharepoint",
"manifest_version": 3,
"author": "Zach Morris",
"action":{
"default_popup": "popup.html",
"default_title": "Open Sharepoint Folder"
},
"background": {
"service_worker": "background.js"
},
"permissions": [
"activeTab",
"tabs",
"scripting",
"notifications"
],
"content_scripts": [{
"js": ["contentScript.js"],
"matches": ["<all_urls>"]
}]
}
popup.html
My popup.html is super simple and really just has a button to press. I included all the .js files in the order I thought necessary
<script src="globalVariable.js"></script>
<script src="contentScript.js"></script>
<script src="popup.js"></script>
<script src="script.js"></script>
<script src="background.js"></script>
globalVariable.js
This one is straight forward. I need to pull the client's name out of the HTML of the page then use it in an API call when I click the button in popup.js This initializes the variable and uses it as place holder.
var clientInfo = {
name: 'test name'
};
ContentScript.js
I only want to run this if importScripts is not undefined. So I threw it in the if statement. Then I make sure I pulled a client name from the page. If not I throw an error message saying no client was found.
if( 'function' === typeof importScripts) {
importScripts('globalVariable.js');
addEventListener('message', onMessage);
function onMessage(e) {
if(b[0]) {
clientInfo.name = b[0].innerHTML;
alert(clientInfo.name + ' was assigned!');
} else {
alert('There is no client on this screen ' + 'b[0] is ' + b[0] + " clientInfo = " + clientInfo.name);
};
};
} else {
console.log("Your stupid code didn't work. ");
}
popup.js
This one pulls up the globalVariable.js to use the clientInfo. and makes a call to the button in background.js
if( 'function' === typeof importScripts) {
importScripts('globalVariable.js');
addEventListener('message', onMessage);
function onMessage(e) {
const text = clientInfo.name;
const notify = document.getElementById( 'myButton' );
notify.addEventListener( 'click', () => {
chrome.runtime.sendMessage( '', {
type: 'notification',
message: text });
} );
}
}
background.js
Same thing here. I import the globalVariable script to use the global variable. The notification will eventually be replaced with the API call when the rest of the code is working properly. I probably don't need to import the script here to access the variable because I can mass it with the event listener in popup.js, but I put it in here out of desperation.
if( 'function' === typeof importScripts) {
importScripts('globalVariable.js');
addEventListener('message', onMessage);
function onMessage(e) {
// do some work here
chrome.runtime.onMessage.addListener( data => {
if ( data.type === 'notification' ) {
chrome.notifications.create(
'',
{
type: 'basic',
title: 'Notify!',
message: data.message || 'Notify!',
iconUrl: 'notify.png',
}
);
console.log("sent notification");
};
});
}
}
You can have the popup.js listen for a button click and content.js handle all the logic of finding the correct element.
popup.js
document.querySelector('#btn').addEventListener('click', () => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) =>
chrome.tabs.sendMessage(tabs[0].id, { command: 'getClientName' })
);
});
content.js
chrome.runtime.onMessage.addListener((msg, sender, response) => {
if (msg.command === 'getClientName')
findClientName(document.querySelectorAll('h3.page-title'));
});
Example of findClientName function:
const findClientName = async (element) => {
let clientName;
if (element.length > 0) {
element.length === 1
? (clientName = setClientName(element[0]))
: handleMultipleElements(element);
} else {
handleNoClientNameFound();
}
clientName ? await makeAPIRequest(clientName) : null;
};
Try this method instead maybe?
{
var x = 2;
}
so:
{
var clientInfo = {
name: 'test name'
};
}
Not very good at this language, so I thought maybe you're missing the brackets?
Edit: Modified code using https://developer.chrome.com/extensions/devtools#evaluated-scripts-to-devtools as reference. Still no luck.
I'm trying to code a chrome-extension which uses chrome.* API call and save portions of the result in a file. I want to automate everything from the loading of the page to the text file download and hence, I don't want to use the browser.onclick() event.
My current attempt has no effect.
What changes would I need to make?
https://stackoverflow.com/a/16720024
Using the above answer as reference, I attempted the following:
manifest.json
{
"name":"Test Extension",
"version":"0.0.1",
"manifest_version": 2,
"description":"Description",
"permissions":["tabs"],
"background": {
"scripts": ["background.js"]
},
"devtools_page": "devtools.html"
}
background.js
// Background page -- background.js
chrome.runtime.onConnect.addListener(function(devToolsConnection) {
// assign the listener function to a variable so we can remove it later
var devToolsListener = function(message, sender, sendResponse) {
// Inject a content script into the identified tab
chrome.tabs.executeScript(message.tabId,
{ file: message.scriptToInject });
}
// add the listener
devToolsConnection.onMessage.addListener(devToolsListener);
devToolsConnection.onDisconnect.addListener(function() {
devToolsConnection.onMessage.removeListener(devToolsListener);
});
}
devtools.js
var backgroundPageConnection = chrome.runtime.connect({
name: "devtools-page"
});
backgroundPageConnection.onMessage.addListener(function (message) {
// Handle responses from the background page, if any
});
chrome.devtools.network.onRequestFinished.addListener(
function(request) {
chrome.runtime.sendMessage({
string: "Hi",
tabId: chrome.devtools.inspectedWindow.tabId,
scriptToInject: "content.js"
});
}
);
chrome.runtime.sendMessage({
string: "Hi",
tabId: chrome.devtools.inspectedWindow.tabId,
scriptToInject: "content.js"
});
content.js
alert("Hello");
I'm working on a Chrome Extension and have tried to access the storage API. However I get a console error:
notes.js:25 Uncaught TypeError: Cannot read property 'sync' of undefined
at HTMLTextAreaElement.save_notes (notes.js:25)
Commenting out the storage.sync call and uncommenting the privacy.services call (strictly to test a 2nd Chrome API) I get this error instead:
notes.js:30 Uncaught TypeError: Cannot read property 'services' of
undefined at HTMLTextAreaElement.save_notes (notes.js:30)
Anyone know what not being able to access any Chrome API explictly listed in the manifest.json permissions could be a sign of?
Here's my javascript file. Under testing, the error occurs on the "save_notes" function call.
console.log("note.js connected")
///// select the text area
var note_area = document.getElementById('notetext')
//// todo Create a function to save the textarea to local storage
// todo First check & load any previously saved notes
;(function load_notes() {
if (note_area === "") {
chrome.storage.sync.get("stored_obj", function(resultObj) {
note_area.value = resultObj.stored_obj
console.log("Attempted to load notes into a blank note area. Did it run correctly?")
})
}
})()
function save_notes () {
chrome.storage.sync.set({
stored_obj: note_area.value
}, function () {
console.log("Saved into Chrome Storage")
})
// chrome.privacy.services.autofillEnabled.get({}, function(details) {
// if (details.value)
// console.log('Autofill is on!');
// else
// console.log('Autofill is off!');
//});
}
note_area.addEventListener('blur', save_notes)
//// setup a blur handler in notes.html to fire function save_notes
// could not be done (inline JS). I need to add a listener and a function in this file
function console_fire() {
console.log ('fired the console log')
}
Here's my manifest.json file:
{
"manifest_version": 2,
"name": "Mindless Blocker",
"version": "0.1",
"description": "Block mindless distractions and have a place to take notes for followup during free time",
"browser_action": {
"default_icon": "cloud-icon.png",
"default_popup": "index.html"
},
"permissions": [
"storage",
"privacy"
]
}
I post the code below:
manifest.json
{
"manifest_version": 2,
"name": "Demo",
"description": "all_frames test",
"version": "1.0",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["content.js"],
"all_frames": true
}],
"permissions": [
"tabs",
"*://*/*"
]
}
background.js
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
var tabStatus = changeInfo.status;
if (tabStatus == 'complete') {
function return_msg_callback() {
console.log('Got a msg from cs...')
}
chrome.tabs.sendMessage(tabId, {
text: 'hey_cs'
}, return_msg_callback);
}
});
content.js
/* Listen for messages */
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
/* If the received message has the expected format... */
if (msg.text && (msg.text == 'hey_cs')) {
console.log('Received a msg from bp...')
sendResponse('hey_bp');
}
});
Then, if I go to a site that includes multiples cross-origin iFrames, e.g., http://www.sport.es/ you would see that all the iFrames within the page receive the message from the background page but only one of them is able to response back. Is this a normal behavior?
Thanks in advance for your answer.
You send just one message with a direct callback so naturally Chrome can use this response callback just one time (it's a one-time connection to one entity, be it a page or an iframe).
Solution 1: send multiple messages to each iframe explicitly:
manifest.json, additional permissions:
"permissions": [
"webNavigation"
],
background.js
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
.............
// before Chrome 49 it was chrome.webNavigation.getAllFrames(tabId, .....
// starting with Chrome 49 tabId is passed inside an object
chrome.webNavigation.getAllFrames({tabId: tabId}, function(details) {
details.forEach(function(frame) {
chrome.tabs.sendMessage(
tabId,
{text: 'hey_cs'},
{frameId: frame.frameId},
function(response) { console.log(response) }
);
});
});
});
Solution 2: rework your background script logic so that the content script is the lead in communication and let it send the message once it's loaded.
content.js
chrome.runtime.sendMessage({text: "hey"}, function(response) {
console.log("Response: ", response);
});
background.js
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
console.log("Received %o from %o, frame", msg, sender.tab, sender.frameId);
sendResponse("Gotcha!");
});
Communicating between a content script and the background page in a Chrome extension
Content script to background page
Send info to background page
chrome.extension.sendRequest({message: contentScriptMessage});
Receive info from content script
chrome.extension.onRequest.addListener(function(request, sender) {
console.log(request.message);
});
Background page to content script
Send info to content script
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendMessage(tab.id, { message: "TEST" });
});
Receive info from background page
chrome.runtime.onMessage.addListener(function(request, sender) {
console.log(request.message);
});
Instead of messaging, you can use executeScript for your purposes. While the callback's argument is rarely used (and I don't think many know how it works), it's perfect here:
chrome.tabs.executeScript(tabId, {file: "script.js"}, function(results) {
// Whichever is returned by the last executed statement of script.js
// is considered a result.
// "results" is an Array of all results - collected from all frames
})
You can make sure, for instance, that the last executed statement is something like
// script.js
/* ... */
result = { someFrameIdentifier: ..., data: ...};
// Note: you shouldn't do a "return" statement - it'll be an error,
// since it's not a function call. It just needs to evaluate to what you want.
Make sure you make script.js able to execute more than once on the same context.
For a frame identifier, you can devise your own algorithm. Perhaps a URL is enough, perhaps you can use the frame's position in the hierarchy.
I am trying to create a channel to my Google App Engine (Python) server, and there seems to be a problem but I am unsure why. When the user toggles the extension, it authenticates the user. If successful, the server replies with a channel token which I use to create the channel. When I authenticate the user, alert("a") appears, but alert("b") does not which makes me believe there is a problem with the line var channel = new goog.appengine.Channel(msg.token);, but the console does not report an error.
I have also copied the javascript code from here and placed it in my manifest as oppose to putting <script type="text/javascript" src="/_ah/channel/jsapi"></script> in background.html.
//script.js
function authenticate(callback) {
var url = "https://r-notes.appspot.com/init/api/authenticate.json?username=" + username + "&password=" + password;
$.post(url, function(data) {
if (data.status == "200") {
channelToken = data.channeltoken;
if (callback) {
callback();
}
var port = chrome.extension.connect({name: "myChannel"});
port.postMessage({token: channelToken});
port.onMessage.addListener(function(msg) {
console.log(msg.question);
});
}
});
}
//background.html
chrome.extension.onConnect.addListener(function(port) {
port.onMessage.addListener(function(msg) {
alert("a"); //pops up
var channel = new goog.appengine.Channel(msg.token);
alert("b"); //does not pop up
console.log(channel); //display error ' Error in event handler for 'undefined': ReferenceError: goog is not defined '
var socket = channel.open()
socket.onopen = function() {
// Do stuff right after opening a channel
console.log('socket opened');
}
socket.onmessage = function(evt) {
// Do more cool stuff when a channel message comes in
console.log('message recieved');
console.log(evt);
}
});
});
//manifest.json
{
"name": "moot",
"description": "Clicking on the moot button will display a sidebar!",
"version": "0.2.69",
"background_page": "html/background.html",
"browser_action": {
"default_icon": "img/icon_64.png",
"default_title": "moot"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/channelApi.js",
"js/script.js", "js/mootsOnSidebar.js", "js/mootsOnPage.js", "js/authenticate.js", "js/otherFunctions.js",
"js/jquery/jquery-1.7.1.js", "js/jquery/jquery.mCustomScrollbar.js", "js/jquery/jquery-ui.min.js",
"js/jquery/jquery.autosize.js", "js/jquery/jquery.mousewheel.min.js", "js/jquery/jquery.easing.1.3.js",
"js/channel.js"],
"css": ["css/cssReset.css", "css/sidebar.css", "css/onPageCreate.css", "css/onPageExists.css", "css/scrollbar.css", "css/authenticate.css"]
}
],
"permissions": [
"tabs", "contextMenus", "http://*/*", "https://*/"
],
"icons": {
"16": "img/icon_16.png",
"64": "img/icon_64.png"
}
}
EDIT - After doing console.log(channel), I discovered the error ' Error in event handler for 'undefined': ReferenceError: goog is not defined '. I am unsure why I receive this error as I did include the required javascript file as I followed this post.
So the solution is that you need to include the file <script type="text/javascript" src="https://talkgadget.google.com/talkgadget/channel.js"></script> in a HTML page. I placed this on the first row of background.html.
My mistake was saving a local copy of channel.js, and refer to it in manifest.json.
I'm now going to place a copy of channel.js on my server, and refer to my server's copy. I don't think there will be any issues with that.
Make a console log for the value of msg direct between alert("a") and var channel = ...
and inspect the value.