Chrome extension: communication between background and content scripts - javascript

I need to execute a content script from a background script with sending a couple of parameters from the background script to the content one. I explored a couple of help pages like this one ...
https://developer.chrome.com/extensions/content_scripts#pi
... but still have no idea how to organize it. In a Firefox extension, I did the following:
background script excerpt:
browser.tabs.executeScript({
file: "content/login.js"
}).then(messageContent).catch(onError)
}
function messageContent() {
var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
gettingActiveTab.then((tabs) => {
browser.tabs.sendMessage(tabs[0].id, {loginUserName: loginUserName, loginPassword: loginPassword});
});
}
content script excerpt:
function justDoTheJob(request, sender, sendResponse) {
var doc = window.content.document;
doc.getElementById("loginUserName").value = request.loginUserName;
doc.getElementById("loginPassword").value = request.loginPassword;
}
browser.runtime.onMessage.addListener(justDoTheJob);
But when I do something like that in Chrome, I get the following:
Error in response to tabs.query: TypeError: Cannot read property 'then' of undefined at Object.callback
So it looks like I am using a wrong syntax or even wrong structure at all. Could you please give me some clue on how to do it properly?
Thanks,
Racoon

As #qwOxxOm points out in comments, you need to use callbacks in Chrome, for example, instead of appending with then(), move the function inside then to the argument chain of the call itself. Otherwise it's used pretty much the same way:
chrome.tabs.executeScript({file: "content/login.js"}, myCallback);
function callback(result) {
// handle result here
}
or like:
function messageContent() {
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
// sendMessage here
})
}
etc.
Error handling is a bit different as you would check lastError instead of using a callback for it.
You can also use the chrome namespace for Firefox (there are some differences between the two browsers/namespaces in some areas you need to take into consideration).

Related

Getting Chrome's current domain name in Typescript

I'm trying to get the currently displayed url from an Angular2 Typescript Chrome Extension.
I've read this in order to get the url, and this regarding how to call Js functions in Ts.
I also set allowJs to true in my tsconfig.json.
Here's my code :
ngOnInit(){
declare var tabs = chrome.tabs.query({params...},function(tabs) {
return tabs[0];
});
console.log(tabs());
}
This doesn't compile unless I remove the log.
I also tried not to use a function expression call :
declare chrome.tabs.query({'active': true, 'windowId': chrome.windows.WINDOW_ID_CURRENT},
function(tabs){}
);
But it didn't work at all.
I was wondering whether it could be async related, because the code is in the OnInit.
Any ideas ?
Thanks !
Solved it thanks to Nitzan's comment. Here is the code :
declare private getUrl(){
chrome.tabs.query({}, function(tabs) {
console.log(tabs[0]);
});
}
ngOnInit(){
this.getUrl();
}

How can I concisely check a webpage's resources for 404s using casperJS?

I have just started using Phantom/Casper.
So far I can list all the resources that a page has using this code:
casper.on('resource.received', function (resource) {
casper.echo(resource.url);
});
So far so good.
Now im trying to merge this with a chunk of code I gleaned and mashed from the documentation. I wanted to load each resource, and then print out the URL if it was missing:
casper.on('resource.received', function (resource) {
// casper.echo(resource.url);
casper.Open(resource.url, function (resource) {
this.on('http.status.404', function (resource) {
this.echo('missing:' + resource.url);
});
});
});
It's messy, but it's what I've got. It fails to open the resources (ln 3), and the console shows no activity.
How can I rewrite this to iterate over the resources and check them for 404s?
(I am aware in my example that I'm not iterating over the resources, I was tempted to use eachthen(), but It's not clear if I can use general casperJS methods inside the 'test' prototype. Sorry, I hope this wasn't too long)
CasperJS' resource.received is based on PhantomJS' onResourceReceived. As you can see from the documentation, you can simply access resource.status. There is no need to explicitly load the resource.
casper.on('resource.received', function (resource) {
if (resource.stage === "end" && resource.status === 404) {
this.echo('missing:' + resource.url);
};
});
Btw, you probably mean casper.open and not casper.Open.

Pass a parameter to a content script injected using chrome.tabs.executeScript()

How can I pass a parameter to the JavaScript in a content script file which is injected using:
chrome.tabs.executeScript(tab.id, {file: "content.js"});
There's not such a thing as "pass a parameter to a file".
What you can do is to either insert a content script before executing the file, or sending a message after inserting the file. I will show an example for these distinct methods below.
Set parameters before execution of the JS file
If you want to define some variables before inserting the file, just nest chrome.tabs.executeScript calls:
chrome.tabs.executeScript(tab.id, {
code: 'var config = 1;'
}, function() {
chrome.tabs.executeScript(tab.id, {file: 'content.js'});
});
If your variable is not as simple, then I recommend to use JSON.stringify to turn an object in a string:
var config = {somebigobject: 'complicated value'};
chrome.tabs.executeScript(tab.id, {
code: 'var config = ' + JSON.stringify(config)
}, function() {
chrome.tabs.executeScript(tab.id, {file: 'content.js'});
});
With the previous method, the variables can be used in content.js in the following way:
// content.js
alert('Example:' + config);
Set parameters after execution of the JS file
The previous method can be used to set parameters after the JS file. Instead of defining variables directly in the global scope, you can use the message passing API to pass parameters:
chrome.tabs.executeScript(tab.id, {file: 'content.js'}, function() {
chrome.tabs.sendMessage(tab.id, 'whatever value; String, object, whatever');
});
In the content script (content.js), you can listen for these messages using the chrome.runtime.onMessage event, and handle the message:
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
// Handle message.
// In this example, message === 'whatever value; String, object, whatever'
});
There are five general ways to pass data to a content script injected with tabs.executeScript()(MDN):
Set the data prior to injecting the script
Use chrome.storage.local(MDN) to pass the data (set prior to injecting your script).
Inject code prior to your script which sets a variable with the data (see detailed discussion for possible security issue).
Set a cookie for the domain in which the content script is being injected. This method can also be used to pass data to manifest.json content scripts which are injected at document_start, without the need for the content script to perform an asynchronous request.
Send/set the data after injecting the script
Use message passing(MDN) to pass the data after your script is injected.
Use chrome.storage.onChanged(MDN) in your content script to listen for the background script to set a value using chrome.storage.local.set()(MDN).
Use chrome.storage.local (set prior to executing your script)
Using this method maintains the execution paradigm you are using of injecting a script that performs a function and then exits. It also does not have the potential security issue of using a dynamic value to build executing code, which is done in the second option below.
From your popup script:
Store the data using chrome.storage.local.set()(MDN).
In the callback for chrome.storage.local.set(), call tabs.executeScript()(MDN).
var updateTextTo = document.getElementById('comments').value;
chrome.storage.local.set({
updateTextTo: updateTextTo
}, function () {
chrome.tabs.executeScript({
file: "content_script3.js"
});
});
From your content script:
Read the data from chrome.storage.local.get()(MDN).
Make the changes to the DOM.
Invalidate the data in storage.local (e.g. remove the key with: chrome.storage.local.remove() (MDN)).
chrome.storage.local.get('updateTextTo', function (items) {
assignTextToTextareas(items.updateTextTo);
chrome.storage.local.remove('updateTextTo');
});
function assignTextToTextareas(newText){
if (typeof newText === 'string') {
Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
el.value = newText;
});
}
}
See: Notes 1 & 2.
Inject code prior to your script to set a variable
Prior to executing your script, you can inject some code that sets a variable in the content script context which your primary script can then use:
Security issue:
The following uses "'" + JSON.stringify().replace(/\\/g,'\\\\').replace(/'/g,"\\'") + "'" to encode the data into text which will be proper JSON when interpreted as code, prior to putting it in the code string. The .replace() methods are needed to A) have the text correctly interpreted as a string when used as code, and B) quote any ' which exist in the data. It then uses JSON.parse() to return the data to a string in your content script. While this encoding is not strictly required, it is a good idea as you don't know the content of the value which you are going to send to the content script. This value could easily be something that would corrupt the code you are injecting (i.e. The user may be using ' and/or " in the text they entered). If you do not, in some way, escape the value, there is a security hole which could result in arbitrary code being executed.
From your popup script:
Inject a simple piece of code that sets a variable to contain the data.
In the callback for chrome.tabs.executeScript()(MDN), call tabs.executeScript() to inject your script (Note: tabs.executeScript() will execute scripts in the order in which you call tabs.executeScript(), as long as they have the same value for runAt. Thus, waiting for the callback of the small code is not strictly required).
var updateTextTo = document.getElementById('comments').value;
chrome.tabs.executeScript({
code: "var newText = JSON.parse('" + encodeToPassToContentScript(updateTextTo) + "');"
}, function () {
chrome.tabs.executeScript({
file: "content_script3.js"
});
});
function encodeToPassToContentScript(obj){
//Encodes into JSON and quotes \ characters so they will not break
// when re-interpreted as a string literal. Failing to do so could
// result in the injection of arbitrary code and/or JSON.parse() failing.
return JSON.stringify(obj).replace(/\\/g,'\\\\').replace(/'/g,"\\'")
}
From your content script:
Make the changes to the DOM using the data stored in the variable
if (typeof newText === 'string') {
Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
el.value = newText;
});
}
See: Notes 1, 2, & 3.
Use message passing(MDN)(send data after content script is injected)
This requires your content script code to install a listener for a message sent by the popup, or perhaps the background script (if the interaction with the UI causes the popup to close). It is a bit more complex.
From your popup script:
Determine the active tab using tabs.query()(MDN).
Call tabs.executeScript()(MDN)
In the callback for tabs.executeScript(), use tabs.sendMessage()(MDN)(which requires knowing the tabId), to send the data as a message.
var updateTextTo = document.getElementById('comments').value;
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.executeScript(tabs[0].id, {
file: "content_script3.js"
}, function(){
chrome.tabs.sendMessage(tabs[0].id,{
updateTextTo: updateTextTo
});
});
});
From your content script:
Add a listener using chrome.runtime.onMessage.addListener()(MDN).
Exit your primary code, leaving the listener active. You could return a success indicator, if you choose.
Upon receiving a message with the data:
Make the changes to the DOM.
Remove your runtime.onMessage listener
#3.2 is optional. You could keep your code active waiting for another message, but that would change the paradigm you are using to one where you load your code and it stays resident waiting for messages to initiate actions.
chrome.runtime.onMessage.addListener(assignTextToTextareas);
function assignTextToTextareas(message){
newText = message.updateTextTo;
if (typeof newText === 'string') {
Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
el.value = newText;
});
}
chrome.runtime.onMessage.removeListener(assignTextToTextareas); //optional
}
See: Notes 1 & 2.
Note 1: Using Array.from() is fine if you are not doing it many times and are using a browser version which has it (Chrome >= version 45, Firefox >= 32). In Chrome and Firefox, Array.from() is slow compared to other methods of getting an array from a NodeList. For a faster, more compatible conversion to an Array, you could use the asArray() code in this answer. The second version of asArray() provided in that answer is also more robust.
Note 2: If you are willing to limit your code to Chrome version >= 51 or Firefox version >= 50, Chrome has a forEach() method for NodeLists as of v51. Thus, you don't need to convert to an array. Obviously, you don't need to convert to an Array if you use a different type of loop.
Note 3: While I have previously used this method (injecting a script with the variable value) in my own code, I was reminded that I should have included it here when reading this answer.
You can use the args property, see this documentation
const color = '#00ff00';
function changeBackgroundColor(backgroundColor) {
document.body.style.backgroundColor = backgroundColor;
}
chrome.scripting.executeScript(
{
target: {tabId},
func: changeBackgroundColor,
args: [color],
},
() => { ... });
Edit: My mistake - This only applies to injected functions, not files as the question specifies.
#RobW's answer is the perfect answer for this. But for you to implement this you need to initiate global variables.
I suggest an alternative for this, which is similar to #RobW's answer. Instead of passing the variable to the file, you load a function from the content.js file and then initiate the function in your current context using the code: and pass variables from current context.
var argString = "abc";
var argInt = 123;
chrome.tabs.executeScript(tabId, { file: "/content.js" }).then(() => {
chrome.tabs.executeScript(tabId, {
allFrames: false,
code: "myFunction('" + argString + "', " + argInt + "); ",
});
});
This is inspired from #wOxxOm's answer here. This method is really going to be helpful to write a common source code for Manifest v2 & v3

Having IE Issues When Passing Object By Ref. and Dynamically Adding Properties

So, I've created a function to do some error checking on an XML file that I recieve from an AJAX call. Part of the validation is that the function then builds out an object for easy access while I process that data into a form. In FF, works like a charm. IE dies with the error:
Object doesn't support this property or method
Here is the function (minus the un-important bits):
function checkReceiveXMLTagString( doc, messageObject, tag ) {
var tag_nodes = doc.getElementsByTagName( tag );
...do some error checking...
messageObject[tag] = tag_str; <-- error occurs on this line
return true;
}
Here is an example of how the function is call:
if ( checkReceiveXMLTagString( messageObject.Name[0], messageObject.Name[0], "First" ) ) {
...process messageObject.Name[0].First...
}
Like I said, FF has no issues. Safari loads the pages as well. IE has issues.
Thanks!
Looks like something is making messageObject be null or undefined
If the error is on this line:
messageObject[tag] = tag_str;
Then, the only two ways I know of that that can cause an error are:
messageObject is not an object that you can set properties on (null or undefined are the most likely ways that would happen)
tag is null or undefined
Since I see that your code calls this function hundreds of times so you can't effectively just break on it, I'd suggest you put in some defensive coding to check for those conditions and output something to the debug console to identify what the state is when the problem occurs. You can even trigger a conditional breakpoint with code like this:
if (!messageObject || !tag) {
debugger;
}
In the toughest cases, you can put an exception handler around it and break when the exception is thrown:
try {
messageObject[tag] = tag_str;
} catch(e) {
debugger;
}
Both of these will allow you to capture the condition in the debugger and examine all your parameters at the time of the error.

Purpose of this javascript, what kind of design patten it used?

if (!window['console']) {
window.console = {
log: function(msg) {}
}
}
$(window).ready(function() {
Site.onReady();
});
var Site = {
host: null,
path: null,
etc..
And there have var Helpers, var Site, looks pretty good, but can't understand the purpose? Anyone who knows that?
if (!window['console']) {
window.console = {
log: function(msg) {}
}
}
This checks to see if there's anything currently assigned to window.console already and if there's not, it assigns a custom object that has a 'log' function. This makes window.console.log usable no matter what, and if there's already a native (or earlier defined) version of the function, it will be used.
$(window).ready(function() {
Site.onReady();
});
var Site = {
host: null,
path: null,
etc..
I have no idea what this is for, but Site is undefined at the time it is placed into the anonymous callback for $(window).ready(), which is something that should be avoided (just place the $(window).ready() below where site is defined)
As for this specific snippet:
$(window).ready(function() {
Site.onReady();
});
this passes an anonymous function to the $(window).ready() function, which will call it when the DOM is ready. Using an anonymous function directly avoids the need to name a function and then pass it in later.
function myFunc() { //we can use myFunc anywhere now, which may be unwanted
Site.onReady();
}
$(window).ready(myFunc);
Lastly:
var Site = {
host: null,
path: null,
etc..
The var myVar = {key1:"value", key2:"other_value"}; syntax creates a new object with keys and values that can be used like this: myVar.key1 = "newValue!"
Looks like it initializes several global objects that are expected on the page. For example console, which is available in Firefox/Firebug for logging, but not other browsers. So by checking for existence of window['console'] and adding it when necessary, you can trust in the JavaScript code you can call console.log() without causing an error.
I assume Site, Helpers, etc all do something similar.
its defining a 'console' object literal on the window object, if it is not already there, which has a function log. This means in your code you can write
console.log('something')
even if the browser doesn't support it.

Categories