I am looking for a function inside a webpage te activate a chrome extension.
Imagine that http://www.example.com/test.html contains:
<script>
hello();
</script>
And my background page contains the definition of the hello function:
function hello() {
alert("test");
}
How can I make sure that the Chrome extension's background page's hello is called when test.html calls hello();?
Before a web page is able to call a background page's function, the following problems need to be solved:
Be able to use hello(); from a web page. This is done by injecting a script defining hello using Content scripts. The injected function communicates with the content script using a custom event or postMessage.
The content script needs to communicate with the background. This is implemented through chrome.runtime.sendMessage.
If the web page needs to receive a reply as well:
Send a reply from the background page (sendMessage / onMessage, see below).
In the content script, create a custom event or use postMessage to send a message to the web page.
In the web page, handle this message.
All of these methods are asynchronous, and have to be implemented through callback functions.
These steps need to be designed carefully. Here's a generic implementation which implements all of the above steps. What you need to know about the implementation:
In the code-to-be-injected, use the sendMessage method whenever the content script need to be contacted.
Usage: sendMessage(<mixed message> [, <function callback>])
contentscript.js
// Random unique name, to be used to minimize conflicts:
var EVENT_FROM_PAGE = '__rw_chrome_ext_' + new Date().getTime();
var EVENT_REPLY = '__rw_chrome_ext_reply_' + new Date().getTime();
var s = document.createElement('script');
s.textContent = '(' + function(send_event_name, reply_event_name) {
// NOTE: This function is serialized and runs in the page's context
// Begin of the page's functionality
window.hello = function(string) {
sendMessage({
type: 'sayhello',
data: string
}, function(response) {
alert('Background said: ' + response);
});
};
// End of your logic, begin of messaging implementation:
function sendMessage(message, callback) {
var transporter = document.createElement('dummy');
// Handles reply:
transporter.addEventListener(reply_event_name, function(event) {
var result = this.getAttribute('result');
if (this.parentNode) this.parentNode.removeChild(this);
// After having cleaned up, send callback if needed:
if (typeof callback == 'function') {
result = JSON.parse(result);
callback(result);
}
});
// Functionality to notify content script
var event = document.createEvent('Events');
event.initEvent(send_event_name, true, false);
transporter.setAttribute('data', JSON.stringify(message));
(document.body||document.documentElement).appendChild(transporter);
transporter.dispatchEvent(event);
}
} + ')(' + JSON.stringify(/*string*/EVENT_FROM_PAGE) + ', ' +
JSON.stringify(/*string*/EVENT_REPLY) + ');';
document.documentElement.appendChild(s);
s.parentNode.removeChild(s);
// Handle messages from/to page:
document.addEventListener(EVENT_FROM_PAGE, function(e) {
var transporter = e.target;
if (transporter) {
var request = JSON.parse(transporter.getAttribute('data'));
// Example of handling: Send message to background and await reply
chrome.runtime.sendMessage({
type: 'page',
request: request
}, function(data) {
// Received message from background, pass to page
var event = document.createEvent('Events');
event.initEvent(EVENT_REPLY, false, false);
transporter.setAttribute('result', JSON.stringify(data));
transporter.dispatchEvent(event);
});
}
});
background.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message && message.type == 'page') {
var page_message = message.message;
// Simple example: Get data from extension's local storage
var result = localStorage.getItem('whatever');
// Reply result to content script
sendResponse(result);
}
});
A Chrome extension is not complete without a manifest file, so here's the manifest.json file which I used to test the answer:
{
"name": "Page to background and back again",
"version": "1",
"manifest_version": 2,
"background": {
"scripts": ["background.js"]
},
"content_scripts": [{
"matches": ["http://jsfiddle.net/jRaPj/show/*"],
"js": ["contentscript.js"],
"all_frames": true,
"run_at": "document_start"
}]
}
This extension was tested at http://jsfiddle.net/jRaPj/show/ (containing hello(); as seen in the question), and shows a dialog saying "Background said: null".
Open the background page, use localStorage.setItem('whatever', 'Hello!'); to see that the message is correctly changed.
There is a builtin solution to Send messages from web pages to the extension
mainfest.json
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
Web page:
// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
if (!response.success)
handleError(url);
});
Extension's background script:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url == blacklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
});
No, with your above code because of background page(s) architecture
Yes with content scripts
Demonstration Using Content Scripts
manifest.json
Registering content scripts myscripts.js
{
"name": "NFC",
"description": "NFC Liken",
"version": "0.1",
"manifest_version": 2,
"permissions": ["tabs", "http://*/", "https://*/"],
"content_scripts": {
"matches": "http://www.example.com/*",
"js": [ "myscript.js"]
},
"browser_action": {
"default_icon": "sync-icon.png",
"default_title": "I Like I Tag"
}
}
Let me know if you need more information.
Related
I am new to using chrome extensions and i have a run into a little problem with setting up a chrome extension. I want the extension to read specific values from a web page and then open up a specific page (form with a number of input fields) from a flask application that I have built in a new tab and then use the values that have been scraped to populate specific fields in the page from my flask app.
I have managed to get the extension to generate a new tab and to load the page from my flask app but I am unable to get the fields to populate. It would seem that the page gets loaded before the fields are populated. I have pasted some code to show you how far I have got. The other issue is that I am using the code parameter from executeScripts to perform the populating action but I don't seem to be able to pass arguments into the code string (I suspect this is not the way to do this but I am working off an answer that I have found very helpful up to this point from here https://stackoverflow.com/a/41094570/1977981
Any help would be much appreciated.
manifest.json
{
"manifest_version": 2,
"name": "My Cool Extension",
"version": "0.1",
"permissions": [
"http://localhost:****/lesson/1/note/new/"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": ["jquery-3.2.1.min.js", "content.js"]
}
],
"browser_action": {
"default_icon": "icon.png"
},
"background": {
"scripts": ["jquery-3.2.1.min.js", "background.js"]
}
}
content.js
// Triggered by sendMessage function in background.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
// listening for request message
if( request.message === "clicked_browser_action" ) {
// Retrieve Lesson title from current tab
var lesson = $('._tabbed-sidebar--title--1Dx5w').find('span').text()
// output this value to the console
console.log(lesson);
// Send a single message to the event listener in your extension i.e. background.js
chrome.runtime.sendMessage({"message": "open_new_tab", "lesson": lesson})
}
}
);
background.js
// Called when the user clicks on the browser action icon.
chrome.browserAction.onClicked.addListener(function(tab) {
// Send a message to the active tab
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
// take the current tab shown in the window
var activeTab = tabs[0];
// Send a message to contents.js - this message is being listened for by contents.js and the runtime.onMessage event is fired in content.js script
chrome.tabs.sendMessage(activeTab.id, {"message": "clicked_browser_action"});
});
});
// listening for "open_new_tab" message from content.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if( request.message === "open_new_tab" ) {
// create new tab but do not activate the tab yet
chrome.tabs.create({"url": "http://localhost:5000/lesson/1/note/new/", active: false }, function(tab){
// load jquery functionality execute script
chrome.tabs.executeScript(tab.id, {file: "jquery-3.2.1.min.js"}, function(results){
chrome.tabs.executeScript(tab.id,{code:`
(function(arguments){
var count = 100; //Only try 100 times
function autofill(){
var lesson = $('.lesson_subsection');
console.log(lesson);
if(lesson){
lesson.value = arguments[0].lesson;
} else {
if(count-- > 0 ){
//The elements we need don't exist yet, wait a bit to try again.
setTimeout(autofill,250);
}
}
}
autofill();
}, request)();
`}, function(results){
chrome.tabs.update(tab.id,{active:true});
}); //END OF second executeScript function
}); // END OF first executeScript function
} // END OF chrome.tabs.create anonymous function
); // END OF chrome.tabs.create
} // END OF if( request.message === "open_new_tab" ) {
}); // END OF addListener anonymous function
Thank you #wOxxOm your comments were very helpful. I was able to use JSON.stringify to load the arguments in to the injected code string. I also had to load the input element from my form using document.getElementsByClassName() instead of using the jquery version of the object. This also meant i didn't have to load the jquery library see line shown below
var less = document.getElementsByClassName('lesson_subsection')[0];
Now my chrome.runtime.onMessage.addListener function in my background.js script is as follows:
// See content.js function
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if( request.message === "open_new_tab" ) {
chrome.tabs.create({"url": "http://localhost:5000/lesson/1/note/new/", active: false }, function(tab){
console.log('Attempting to inject script into tab:',tab);
chrome.tabs.executeScript(tab.id,{code:`
(function(argument){
var count = 100; //Only try 100 times
function autofill(){
var less = document.getElementsByClassName('lesson_subsection')[0];
if(less){
less.value = argument;
} else {
if(count-- > 0 ){
//The elements we need don't exist yet, wait a bit to try again.
setTimeout(autofill,250);
}
}
}
autofill();
})(` + JSON.stringify(request.lesson) + `);
`}, function(results){
// chrome.tabs.sendMessage(tab.id, {"message": "need to update tab", "tab": tab.id});
chrome.tabs.update(tab.id, { active: true });
}); //END OF executeScript function
} // END OF chrome.tabs.create anonymous function
); // END OF chrome.tabs.create
} // END OF if( request.message === "open_new_tab" ) {
}); // END OF addListener anonymous function
I am currently creating a WebExtension in which I register a listener on the web requests being made, as such:
main.js:
chrome.webRequest.onBeforeRequest.addListener(main_function, {urls: sites}, ["blocking"]);
where sites is an array containing a list of URLs loaded from a settings page.
Upon changing these settings, which live in separate HTML and JavaScript files, I want to update the aforementioned listener to now contain the new list of sites.
I'm having trouble getting access to this listener from the Settings JavaScript file, as
onBeforeRequest.removeListener(callback)
requires the original callback as an argument.
Does anyone know how I can, from a different JavaScript file, update this listener?
Example of problem (3 files):
manifest.json:
{
...
"permissions": ["webRequest", "webRequestBlocking", "storage"],
"background: { "scripts": ["main.js"] },
"options_ui": { "page":"settings.html" }
}
where settings.html in turn loads settings.js.
main.js:
/* I wont type out the code to load this array from settings. You will have to imagine it being loaded */
all_urls = ["http://www.google.com", "http://www.stackoverflow.com"];
function example_callback() {
console.log("Hello World!");
}
chome.webRequest.onBeforeRequest.addListener(example_callback, {urls: all_urls}, ["blocking"]);
settings.js:
/* Again, imagine that the settings are edited by this file and saved to storage. */
all_urls = ["https://www.google.com"];
/* This is where the problem lies. Listener cannot be removed, as callback is not available in this file */
chrome.webRequest.onBeforeRequest.removeListener(); // Wont work
chrome.webRequest.onBeforeRequest.addListener(example_callback, {urls: all_urls}, ["blocking"]);
Communicating between your options page (or panel) JavaScript and background scripts
There are two general methods of communicating between your options page or a panel, and your background scripts.
You can directly access variables and functions that are in your background scripts from your options page JavaScript by first obtaining the Window object for your background scripts by using extension.getBackgroundPage(). If you want to directly access other pages from your background script you can use extension.getViews() to get the Window object for the background page and, if defined, any popup/panel, option page, or tab containing content which is packaged with the extension.
For the code that is in the question you could do:
let backgroundPage = chrome.extension.getBackgroundPage();
chrome.webRequest.onBeforeRequest.removeListener(backgroundPage.example_callback);
You can send messages back an forth between the pages using runtime.sendMessage(), runtime.onMessage, and/or runtime.connect().
If you send messages back and forth, then you need to make choices as to what those messages will be used for and their contents. Do you send all the data, or just a message that the data was updated? Are you going to use messages for multiple purposes? If so, how are your listener(s) going to determine what message is for which part of your script. You will need to impose some type of format on the messages. The more things that you need to accomplish with these messages, the more complex the format that you need to impose.
Example Code:
The following extension logs web requests to the console. Depending on the users choice, it will log
Nothing
All requests to mozilla.org
All web requests
It implements the same page as both an options_ui page and a default_popup for a browser_action button. The user can select from the above 3 logging options and how the option data is communicated to the background page:
Options are stored to storage.local in the options.js code. Then, the options.js directly invokes the getOptions() function in the background.js file to have the background script re-read the options.
Options are stored to storage.local in the options.js code. Then, the options.js sends a optionsUpdated message to the background script that the options have been updated. The background script then re-reads the options.
A optionsData message is sent from the options.js code to the background page when the options are change which contains a data payload with all of the options. The options are then stored to storage.local in the background script. Once the options are stored, the background script sends a optionsStored message back to the options.js code. The options.js code then indicates to the user that the options have been saved.
Messages that are sent between the background.js and options.js are an object that has the following format:
{
type: //String describing the type of message:
// 'optionsUpdated' 'optionsData', or 'optionsStored'
data: //Object containing the options data
}
//Options data object:
{
loggingUrls: //Array of URL match strings for webRequest requestFilter
useDirect: //Number: 0, 1, 2 indicating the method of communication between
// options.js and background.js
// 0 = Directly invoke functions in background script from options/panel code
// 1 = Send a message that data was updated
// 2 = Send a message with all options data
}
The extension has been testing in both Firefox and Google Chrome:
manifest.json:
{
"description": "Demonstrate Changing webRequest.RequestFilter",
"manifest_version": 2,
"name": "webrequest.requestfilter-demo",
"version": "0.1",
"applications": {
"gecko": {
//Firefox: must define id to use option_ui:
"id": "webrequestrequestfilter-demo#example.example",
"strict_min_version": "42.0",
"strict_max_version": "51.*"
}
},
"permissions": [
"storage",
"webRequest",
"webRequestBlocking",
"<all_urls>" //Required for Google Chrome. Not, currently, needed for Firefox.
],
"background": {
"scripts": [
"background.js"
]
},
"browser_action": {
"default_icon": {
"48": "myIcon.png"
},
"default_title": "Currently NOT logging. Click to start logging only mozilla.org",
"browser_style": true,
"default_popup": "options.html"
},
"options_ui": {
"page": "options.html",
"chrome_style": true
}
}
background.js:
var webRequestExtraInfo = ["blocking"];
var useDirect=0; //Holds the state of how we communicate with options.js
const useDirectTypes=[ 'Directly invoke functions in background script'
,'Send a message that data was updated'
,'Send a message with all options data'];
//Register the message listener
chrome.runtime.onMessage.addListener(receiveMessage);
function receiveMessage(message,sender,sendResponse){
//Receives a message that must be an object with a property 'type'.
// This format is imposed because in a larger extension we may
// be using messages for multiple purposes. Having the 'type'
// provides a defined way for other parts of the extension to
// both indicate the purpose of the message and send arbitrary
// data (other properties in the object).
console.log('Received message: ',message);
if(typeof message !== 'object' || !message.hasOwnProperty('type')){
//Message does not have the format we have imposed for our use.
//Message is not one we understand.
return;
}
if(message.type === "optionsUpdated"){
//The options have been updated and stored by options.js.
//Re-read all options.
getOptions();
}
if(message.type === "optionsData"){
saveOptionsSentAsData(message.data,function(){
//Callback function executed once data is stored in storage.local
console.log('Sending response back to options page/panel');
//Send a message back to options.js that the data has been stored.
sendResponse({type:'optionsStored'});
//Re-read all options.
getOptions();
});
//Return true to leave the message channel open so we can
// asynchronously send a message back to options.js that the
// data has actually been stored.
return true;
}
}
function getOptions(){
//Options are normally in storage.sync (sync'ed across the profile).
//This example is using storage.local.
//Firefox does not currently support storage.sync.
chrome.storage.local.get({
loggingUrls: [''],
useDirect: 0
}, function(items) {
if(typeof items.useDirect !== 'number' || items.useDirect<0 || items.useDirect>2) {
items.useDirect=0;
}
useDirect = items.useDirect;
updateLogging(items.loggingUrls);
console.log('useDirect=' + useDirectTypes[useDirect]);
});
}
function saveOptionsSentAsData(data,callback) {
//Options data received as a message from options.js is
// stored in storeage.local.
chrome.storage.local.set(data, function() {
//Invoke a callback function if we were passed one.
if(typeof callback === 'function'){
callback();
}
});
}
function updateLogging(urlArray){
//The match URLs for the webRequest listener are passed in as an
// array. Check to make sure it is an array, and forward to
// function that adds the listener as a requestFilter.
if(typeof urlArray === "object" && Array.isArray(urlArray)
&& urlArray[0].length>0){
startListeningToWebRequests({urls: urlArray});
}else{
//The argument was not an array
stopListeningToWebRequests();
}
}
function logURL(requestDetails) {
//Log the webRequest to the Console.
console.log("Loading: " + requestDetails.url);
return {}; //Return object in case this is a blocking listener
}
function stopListeningToWebRequests() {
if(chrome.webRequest.onBeforeRequest.hasListener(logURL)) {
//Don't really need to check for the listener, as removeListener for a
// function which is not listening does nothing (no-op).
chrome.webRequest.onBeforeRequest.removeListener(logURL);
console.log("STOPPED logging all Web Requests");
}
}
function startListeningToWebRequests(requestFilter) {
stopListeningToWebRequests();
//Start listening to webRequests
chrome.webRequest.onBeforeRequest
.addListener(logURL,requestFilter,webRequestExtraInfo);
//Log to the console the requestFilter that is being used
console.log("Logging Web Requests:", requestFilter, "-->", requestFilter.urls);
}
//Read the options stored from prior runs of the extension.
getOptions();
//On Firefox, open the Browser Console:
//To determine if this is Chrome, multiple methods which are not implemented
// in Firefox are checked. Multiple ones are used as Firefox will eventually
// support more APIs.
var isChrome = !!chrome.extension.setUpdateUrlData
&& !!chrome.runtime.reload
&& !!chrome.runtime.restart;
if(!isChrome) {
//In Firefox cause the Browser Console to open by using alert()
window.alert('Open the console. isChrome=' + isChrome);
}
options.js:
// Saves options to chrome.storage.local.
// It is recommended by Google that options be saved to chrome.storage.sync.
// Firefox does not yet support storage.sync.
function saveOptions(data, callback) {
chrome.storage.local.set(data, function() {
if(typeof callback === 'function'){
callback();
}
// Update status to let user know options were saved.
notifyOptionsSaved();
});
}
function optionsChanged() {
//Get the selected option values from the DOM
let loggingUrls = document.getElementById('loggingUrls').value;
let useDirectValue = document.getElementById('useDirect').value;
useDirectValue = +useDirectValue; //Force to number, not string
//Put all the option data in a single object
let optionData = {
loggingUrls: [loggingUrls],
useDirect: useDirectValue
}
if(useDirectValue == 0 ) {
//We save the options in the options page, or popup
saveOptions(optionData, function(){
//After the save is complete:
//The getOptions() functon already exists to retrieve options from
// storage.local upon startup of the extension. It is easiest to use that.
// We could remove and add the listener here, but that code already
// exists in background.js. There is no reason to duplicate the code here.
let backgroundPage = chrome.extension.getBackgroundPage();
backgroundPage.getOptions();
});
} else if (useDirectValue == 1) {
//We save the options in the options page, or popup
saveOptions(optionData, function(){
//Send a message to background.js that options in storage.local were updated.
chrome.runtime.sendMessage({type:'optionsUpdated'});
});
} else {
//Send all the options data to background.js and let it be dealt with there.
chrome.runtime.sendMessage({
type:'optionsData',
data: optionData
}, function(message){
//Get a message back that may indicate we have stored the data.
if(typeof message === 'object' && message.hasOwnProperty('type')){
if(message.type === 'optionsStored') {
//The message received back indicated the option data has
// been stored by background.js.
//Notify the user that the options have been saved.
notifyOptionsSaved();
}
}
});
}
}
// Restores select box using the preferences
// stored in chrome.storage.
function useStoredOptionsForDisplayInDOM() {
chrome.storage.local.get({
loggingUrls: [''],
useDirect: 0
}, function(items) {
//Store retrieved options as the selected values in the DOM
document.getElementById('loggingUrls').value = items.loggingUrls[0];
document.getElementById('useDirect').value = items.useDirect;
});
//notifyStatusChange('Option read');
}
function notifyOptionsSaved(callback){
//Notify the user that the options have been saved
notifyStatusChange('Options saved.',callback);
}
function notifyStatusChange(newStatus,callback){
let status = document.getElementById('status');
status.textContent = newStatus;
//Clear the notification after a second
setTimeout(function() {
status.textContent = '';
if(typeof callback === 'function'){
callback();
}
}, 1000);
}
document.addEventListener('DOMContentLoaded', useStoredOptionsForDisplayInDOM);
document.getElementById('optionsArea').addEventListener('change',optionsChanged);
options.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebRequest Logging Options</title>
<style>
body: { padding: 10px; }
</style>
</head>
<body>
<div id="optionsArea">
Log Web Requests from:
<select id="loggingUrls">
<option value="">None</option>
<option value="*://*.mozilla.org/*">Mozilla.org</option>
<option value="<all_urls>">All URLs</option>
</select>
<br/>
Communication with background page:
<select id="useDirect">
<option value="0">Direct</option>
<option value="1">Message Updated</option>
<option value="2">Message all Data</option>
</select>
</div>
<div id="status" style="top:0px;display:inline-block;"></div>
<script src="options.js"></script>
</body>
</html>
The code in this answer was combined and modified from that in the question I answered here, and my answer here, which is based on code the OP for that question provided in a GitHub repository. The options.js and options.html files had their start with code found on developer.chrome.com.
The solution is to use the WebExtension message API, as such:
settings.js:
...
/* Settings now outdated */
chrome.runtime.sendMessage(message);
...
main.js
...
chrome.runtime.onMessage.addListener( (message) => {
/* Update listener */
chrome.webRequest.onBeforeRequest.removeListener(example_callback);
chrome.webRequest.onBeforeRequest.addListener(example_callback, {urls: all_urls}, ["blocking"]);
});
...
Relevant section from documentation: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts#Communicating_with_background_scripts
I am developing a chrome extension, here are the main files:
background.js
getPageDimension = function (){
chrome.tabs.getSelected(null, function(tab) {
chrome.tabs.sendMessage(tab.id, { message: "DIMENSION" }, function(response){
if (response != null) {
console.log(response.x);
console.log(response.y);
console.log(response.w);
console.log(response.h);
}else{
console.log('Response is null');
}
});
});
};
content.js
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (msg.message && (msg.message == "DIMENSION")) {
var dimension = getPageDiemension(document.documentElement);
console.log(dimension.x);
console.log(dimension.y);
console.log(dimension.w);
console.log(dimension.h);
sendResponse({x: dimension.x, y: dimension.y,w: dimension.w,h: dimension.h});
}
});
getPageDiemension = function(currentDom){
var dimension = new Object();
dimension.x = 0;
dimension.y = 0;
dimension.w = currentDom.scrollWidth;
dimension.h = currentDom.scrollHeight;
return dimension;
}
So my aim is to get the full height and width of page loaded in current active tab. When I debug my content script, I get the proper response in my background.js, but if run the script without debugging, I get an undefined response in my background.js.
Here is the declaration of my cotent.js in my manifest.json file:
"content_scripts": [{
"all_frames": true,
"matches": [
"http://*/*", "https://*/*"
],
"js": ["content.js"]
}],
Kindly help me, where am I going wrong?. Let me know if you need any further data.
Issues
There are two little problems in your code, which I found after a wile, since that it looks perfectly working (when it isn't at all).
You're using the deprecated chrome.tabs.getSelected(...) method, which you should avoid. Use chrome.tabs.query({active: true, highlighted: true}, ...) instead.
In your chrome.runtime.onMessage listener, in the content script, you are doing some calculations before sending the response: this makes the message channel close before you can send a response, and will result in a null response.
Quoting from the official documentation:
This function (sendResponse) becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).
Solution
Now that you know what the problems are, you can easily replace the old tabs.getSelected() with tabs.query(), and add a return true; statement in the handler of the runtime.onMessage event in your content script. The solution is the following, I also lightened the code a bit.
Your background.js:
getPageDimension = function (){
chrome.tabs.query({active: true, highlighted: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, { message: "DIMENSION" }, function(response){
if (response !== null) console.log('Response:', response);
else console.log('Response is null');
});
});
};
Your content.js:
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (msg.message && (msg.message == "DIMENSION")) {
var dimension = getPageDiemension(document.documentElement);
console.log('Dimension:', dimension);
sendResponse(dimension);
}
return true;
});
getPageDiemension = function(currentDom){
return { x: 0, y: 0,
w: currentDom.scrollWidth,
h: currentDom.scrollHeight
}
}
Working example
You can find a working example I made for testing HERE.
Documentation links
Just for clarity, I'm leaving you some documentation links regarding the APIs involved in this extension that you may find helpful:
chrome.tabs API
chrome.tabs.query method
Chrome extension message passing
chrome.tabs.sendMessage method
chrome.runtime.onMessage event
I am having some weird results with message passing. I have a content script interacting with a script embedded in my popup page. Currently, they are repeatedly sending the same messages and responses to eachother. I feel like this has something to do with my event listeners firing too much, or maybe the methods i've written to pass the messages are creating some sort of accidental message loop.
My popup script has some logic to inject my content script into every webpage the user visits:
document.addEventListener('DOMContentLoaded', function () {
chrome.tabs.onUpdated.addListener(contextSwitch);
});
//after navigating to a new url
function contextSwitch() {
var tabid = arguments[0];
var changeinfo = arguments[1];
var tab = arguments[2];
//keeps the script from being injected into a tab that's still loading.
if(changeinfo.status === "complete"){
chrome.tabs.executeScript(tabid, {file : "inject.js"}, postInject);
}
else if (changeinfo.status ==="loading"){
console.log("loading fired");
}
else{
console.log("Something went wrong.");
console.log(changeinfo);
}
}
//this fires after the script is injected.
function postInject() {
chrome.runtime.onConnect.addListener(function(port) {
port.onMessage.addListener(messageBroker);
port.postMessage({status:"connected"});
});
}
//helps pick and choose what to do based on nthe message received
function messageBroker() {
var msg = arguments[0];
var sender = arguments[1];
if(msg.status == "initialized"){
sender.postMessage({status:"inject",contents:{notes:page.Notes}});
}
}
The content script opens up a port like this:
var extPort = chrome.runtime.connect({
name : "CS:" + window.location.href
});
extPort.onMessage.addListener(function() {
var msg = arguments[0];
var sender = arguments[1];
//the popup page sends the "connected" message after the port connects.
if (msg.status === "connected") {
var initmsg = {
status : "initialized",
contents : {
url : window.location.href
}
};
sender.postMessage(initmsg);
}
if (msg.status === "inject") {
var contents = msg.contents;
if (contents) {
for (item in contents) {
//do stuff with the contents
}
}
}
sender.postMessage({
status : "received"
});
});
Here is my manifest file:
{
"name": "name",
"version": "0.0.1",
"description": "chrome extension",
"permissions": [
"activeTab",
"tabs","<all_urls>","http://*/","https://*/"
],
"browser_action": {
"default_title": "ext",
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"background": {
"scripts": ["chrome.js"],
"persistent": false
},
"manifest_version": 2,
"content_security_policy": "script-src 'self' https://ajax.googleapis.com; object-src 'self'"
}
My background script is embedded into my popup like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>popup</title>
<meta name="description" content="">
<meta name="author" content="alex">
<script src="chrome.js"></script>
</head>
<body style="width:400px;height:400px;">
<div id=menu> Menu </div>
button1
button2
</body>
</html>
My goal for this back and forth message passing is this:
inject the script into any appropriate page
the script tells the background that it's been injected and is ready to receive orders.
the background page gives it orders.
the content script performs some operation based upon those orders.
If I understand correctly, the problem is that you have been mixing up the packground-page and popup code.
The popup page is executed every time you open the popup. This means every time the popup is shown, a couple of new listeners are registered (which continue to live on after the popup is closed). Thus you end up with a dozen identical listeners, responding to an event (which in turn results is moe and most importantly unexpected traffic).
I suggest you take a look at the core concepts of an extension, to help clear things up:
Event Pages (a.k.a. non-persistent Background Pages)
Popups
Content Scripts
I am creating an extension that I need to have the ability to run the content script multiple times when clicking on the page action button. I have this in my background.js:
chrome.pageAction.onClicked.addListener(function(tab) {
alert('calling content_script');
chrome.tabs.executeScript(null, {
file: 'content_script.js'
},function(){alert("success");});
});
This works the first time the button is clicked. When clicked a second time, I get my popup saying "calling content_script" but the content script is never executed. Why is this?
Here is the background script in its entirety:
function checkForValidUrl(tabId, ChangeInfo, tab){
if(tab.url.indexOf("tiger.armstrong")> -1){
chrome.pageAction.show(tabId);
if(tab.url.indexOf("tiger.armstrong") == 0){
chrome.pageAction.hide(tabId);
}
}
}
chrome.tabs.onUpdated.addListener(checkForValidUrl);
chrome.pageAction.onClicked.addListener(function(tab) {
alert('calling content_script');
chrome.tabs.executeScript(null, {
file: 'content_script.js'
},function(){alert("success");});
});
Here is the manifest:
{
"name": "LiveLab Post Grades",
"version": "2.0",
"permissions": [
"activeTab","tabs","http://*/*","https://*/*"
],
"background": {
"scripts": ["jquery.min.js","background3.js"],
"persistent": false
},
"page_action": {
"default_icon": {
"19": "GIcon.png"
},
"default_title": "LiveLab Tools"
},
"content_scripts": [ {
"js": [ "jquery.min.js" ],
"matches": [ "http://*/*", "https://*/*"],
"run_at": "document_end"
}],
"manifest_version": 2
}
Here is the content script:
var livelabtools = {
/**
* this function is like window.open, but it can POST (rather than GET) from js
* source: http://canop.org/blog/?p=426
*/
canop_open: function (verb, url, data, target) {
var form = document.createElement("form");
form.action = url;
form.method = verb;
form.target = target || "_self";
if (data) {
//for (var key in data) {
var input = document.createElement("input");
input.name = 'data';
input.value = data;//typeof data[key] === "object" ? JSON.stringify(data[key]) : data[key];
form.appendChild(input);
//console.log(form);
//}
}
// these two lines are only needed for ie
//form.style.display = 'none';
//document.body.appendChild(form);
form.submit();
console.log("form submit === " + form);
form.remove();
},
post_grades: function () {
alert('in post grades!!!!');
var str, exercise,
i = 0;
grades = {};
do {
ex_str = "form1:tabSet1:tabInstr:lp2:tabSet4:tabp:lpProgress:ts1:tab7:lp7:table3:rg3:" + i + ":tc3:st3";
lname_str = "form1:tabSet1:tabInstr:lp2:tabSet4:tabp:lpProgress:ts1:tab7:lp7:table3:rg3:" + i + ":tc1:st1";
grade_str = "form1:tabSet1:tabInstr:lp2:tabSet4:tabp:lpProgress:ts1:tab7:lp7:table3:rg3:" + i + ":tc7:st7_field";
exercise = document.getElementById(ex_str);
lname = document.getElementById(lname_str);
grade = document.getElementById(grade_str);
if (exercise != null) {
if (grades[lname.innerHTML] === undefined)
grades[lname.innerHTML] = {};
console.log(lname.innerHTML + ", " + exercise.innerHTML + ", " + grade.innerHTML);
if (grade.value != null && grade.value != '')
grades[lname.innerHTML][exercise.innerHTML] = grade.value;
else
grades[lname.innerHTML][exercise.innerHTML] = "0";
}
i++;
} while (exercise != null);
// console.log(JSON.stringify(grades));
// console.log(JSON.stringify(grades).length)
//window.open("http://aspen2.cscofc.info/jsontocsv.php?data="+JSON.stringify(grades));
console.log('posting...' + "\n JSON.String... = "+ JSON.stringify(grades));
livelabtools.canop_open("post", "http://aspen2.cscofc.info/jsontocsv.php", JSON.stringify(grades));
console.log('done');
return "function end";
}
}
console.log(livelabtools.post_grades());
I won't go into detail about it, unless asked but the important parts to note are the return statement and the console log. Everything runs perfectly fine the first time the page action button is clicked, and when finished, I get "function end" printed to the console. After the initial run, however, whenever I click on the page action button, I get an alert saying "calling content_script" and nothing else happens. Why won't my content script run more than once?
It seems like when a script has already been injected it is not injected again.
So, the way to go would be to initiate your action by passing a message to the content script. (Of course, if the content script has not been injected yet, you need to inject it first.)
One solution I came up with (and after testing confirmed it works fine) is the following:
On pageAction.onClicked send a message from the background page to the content script, asking it to do something (e.g. post the grades). Also, request a response to be sent back to the background page.
[See, also, chrome.tabs.sendMessage(...).]
If the content script has already been injected, have it receive the message, send a confirmation back to the background page and proceed doing something (e.g. posting the grades). This process can take place as many times as you want.
[See, also, chrome.runtime.onMessage.]
The first time that the pageAction.onClicked is fired, there will be no content script listening for messages. In that case, there will be no message confirmation. Instead chrome.runtime.lastError will be set. In that case, the background page will have to inject the content script first and then send the message again.
[See, also, chrome.runtime.lastError.]
Theoreticaly speaking, that should do it !
Practicaly, this is the sample code that worked for me:
manifest.json:
(Note: If you have more specific requirements regarding the pages you need to access, you could incorporate them into the manifest and get rid of some of the permissions.)
{
...
"background": {
"persistent": false,
"scripts": ["background.js"]
},
"page_action": {
"default_title": "Test Extension"
},
"permissions": [
"tabs",
"http://*/*",
"https://*/*"
]
...
}
background.js:
function checkForValidURL(tabId, info, tab) {
var idx = tab.url.indexOf("tiger.armstrong");
if (idx > 0) {
chrome.pageAction.show(tabId);
} else {
chrome.pageAction.hide(tabId);
}
}
chrome.tabs.onUpdated.addListener(checkForValidURL);
function onPageActionClicked(tab) {
// Send message to content script, asking to post grades
alert("Calling content_script...");
chrome.tabs.sendMessage(tab.id, { action: "postGrades" }, function() {
if (chrome.runtime.lastError) {
// The error indicates that the content script
// has not been injected yet. Inject it and...
chrome.tabs.executeScript(tab.id, {
file: "content.js"
}, function() {
if (!chrome.runtime.lastError) {
// ...if injected successfully, send the message anew
onPageActionClicked(tab);
}
});
} else {
// The content script called our response callback,
// confirming that it is there and got our message
alert("Message got through !");
}
});
};
chrome.pageAction.onClicked.addListener(onPageActionClicked);
content.js:
var livelabtools = {
/**
* This function is like window.open, but it can POST (rather than GET) from
* JS source: http://canop.org/blog/?p=426
*/
canop_open: function (method, url, data, target) {
var form = document.createElement("form");
form.action = url;
form.method = method;
form.target = target || "_self";
// 'data' is an object with key-value pairs
// of fields to be sent
if (data) {
for (var key in data) {
var input = document.createElement("input");
input.name = key;
input.value = (typeof(data[key]) === "object")
? JSON.stringify(data[key]) : data[key];
form.appendChild(input);
}
}
form.submit();
form.remove();
},
post_grades: function () {
console.log("Posting some grades...");
livelabtools.canop_open("POST",
"http://aspen2.cscofc.info/jsontocsv.php",
"{}");
console.log("Grades sent !");
}
}
// Listen for messages from the background page
// (It actually listens for messages from anyone in the context,
// but the background page is the one that interrests us)
chrome.runtime.onMessage.addListener(function(msg, sender, response) {
// If we've been asked to post grades...
if (msg.action && (msg.action == "postGrades")) {
// ...confirm we got the message and...
response();
// ...do what we do best: post grades !
livelabtools.post_grades();
}
});
Let's hope this covers it :)