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

5

81

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"});
Caerleon answered 10/7, 2013 at 9:55 Comment(0)
A
135

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'
});
Angelangela answered 11/7, 2013 at 10:40 Comment(9)
I am only getting a message object no text.Caerleon
chrome.extension.onMessage.addListener(function (response, sender) { //Selected Text var text = document.getElementById('text'); text.innerHTML = response.data; //Needs to get this data in js file It has selected Text in any tab chrome.tabs.executeScript(tab.id, { code: 'var config = 788888888 ;' }, function() { chrome.tabs.executeScript(tab.id, {file: 'content.js'}); }); });Caerleon
@Caerleon I have shown two different methods to achieve the same goal.Angelangela
I have tried these methods the first one works for me,How i can put my own data inside var config only integers get accepted,In second method message object how i can get text value for that.Caerleon
@Caerleon Please read my answer carefully. After the first example, I have shown how to pass a custom value...Angelangela
Thanks Rob, But i can only get a object as alert in content.js,here is the code var response = response.data; var config = {somebigobject:'response'}; chrome.tabs.executeScript(tab.id, { code: 'var config = ' + JSON.stringify(config) }, function() { chrome.tabs.executeScript(tab.id, {file: 'content.js'}); });where i am wrong?Caerleon
@Caerleon Yes, because you have to read the property of that object. In this example, config.somebigobject. Note that "somebigobject" can be anything, I just wanted to show how to do this in general, and by choosing an unlikely name like "somebigobject", I wanted to convey the message that you can put anything you want on that object.Angelangela
@RobW, Re last para, is there anyway to get window.onMessage to work instead of chrome.runtime.onMessage.addListener?Lenoir
@Lenoir What do you mean by get window.onMessage to work? The global window.onmessage event is triggered by calling window.postMessage. A content script can call this method, but the page will also be notified, which may not be what you want.Angelangela
P
26

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
    1. Use chrome.storage.local(MDN) to pass the data (set prior to injecting your script).
    2. Inject code prior to your script which sets a variable with the data (see detailed discussion for possible security issue).
    3. 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
    1. Use message passing(MDN) to pass the data after your script is injected.
    2. 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:

  1. Store the data using chrome.storage.local.set()(MDN).
  2. 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:

  1. Read the data from chrome.storage.local.get()(MDN).
  2. Make the changes to the DOM.
  3. 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:

  1. Inject a simple piece of code that sets a variable to contain the data.
  2. 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:

  1. 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:

  1. Determine the active tab using tabs.query()(MDN).
  2. Call tabs.executeScript()(MDN)
  3. 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:

  1. Add a listener using chrome.runtime.onMessage.addListener()(MDN).
  2. Exit your primary code, leaving the listener active. You could return a success indicator, if you choose.
  3. Upon receiving a message with the data:
    1. Make the changes to the DOM.
    2. 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.

Pyrrolidine answered 26/11, 2016 at 6:8 Comment(4)
You sure about "will execute scripts in the order in which you cal.."? Isn't the order undefined?Lenoir
@Pacerier, All of my testing (other project, not just for this answer) showed (same runAt) scripts are injected in the order tabs.executeScript() is called (both Chrome & Firefox). I don't recall if I looked through the source code on both to verify. An important aspect of this is that injected scripts come from within your extension, not network resources (where most async delay would be). You're correct that it's not guaranteed in the documentation, which is why I wrote it such that it does actually wait for the tabs.executeScript() callback to inject the second, non-code: script.Pyrrolidine
@Pacerier, Most reasonable ways to implement such code might not guarantee that the injection occurs in the order tabs.executeScript() is executed, but would make it unlikely that it would not be in that order. For example: tabs.executeScript() probably enters a message instructing the injection in a message queue to be sent to the correct process handling the content. Once received in that process, it would enter a queue for injection (separate queue's per runAt, or at least organized by runAt), or be immediately processed (e.g. for code: that is immediately available). etc.Pyrrolidine
@Pacerier, In this instance, the code: script is immediately available (does not even need to be fetched from within your extension (i.e. disk, or RAM cache)). This means that it is even more likely to be injected before the other script. However, the testing I did indicated that even for multiple (same runAt) scripts fetched from within the extension, the scripts were injected in the order tabs.executeScript() is called. Again, it's not guaranteed, but it works at least 99%+ of the time. But, that does not mean you should not wait for the callback, when you have a hard dependency.Pyrrolidine
P
15

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.

Perversion answered 9/10, 2021 at 15:1 Comment(0)
P
0

@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

Platino answered 4/9, 2022 at 8:29 Comment(0)
W
0

With Manifest v3 and depreciation of chrome.tabs.executeScript and the fact that chrome.scripting doesn't support code as a string the new option is arising in Chrome 120: https://developer.chrome.com/docs/extensions/reference/api/userScripts

const dead = 'back';
chrome.userScripts.register([{
    id: 'test',
    matches: ['*://*/*'],
    allFrames: true,
    runAt: 'document_start',
    world: 'MAIN',
    js: [{code: `console.log('eval is ${dead}, baby!');`}]
}], 
() => {
    resolve(true);
});

Currently, you have to enable developer mode at chrome://extensions/ but, I guess, it will be soon available by default.

Wadleigh answered 15/1 at 20:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.