Pass a Parameter to a Content Script Injected Using Chrome.Tabs.Executescript()

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

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'
});

tabs.executeScript - passing parameters and using libraries?

If you don't want to use messaging then:

chrome.tabs.executeScript(tabId, {file: "jquery.js"}, function(){
chrome.tabs.executeScript(tabId, {code: "var scriptOptions = {param1:'value1',param2:'value2'};"}, function(){
chrome.tabs.executeScript(tabId, {file: "script.js"}, function(){
//all injected
});
});
});

(jquery.js should be placed into extension folder). Script options will be available inside scriptOptions variable in the script.js.

With messaging it is just as easy:

chrome.tabs.executeScript(tabId, {file: "jquery.js"}, function(){
chrome.tabs.executeScript(tabId, {file: "script.js"}, function(){
chrome.tabs.sendMessage(tabId, {scriptOptions: {param1:'value1',param2:'value2'}}, function(){
//all injected
});
});
});

You would need to add a request listener to script.js:

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
var scriptOptions = message.scriptOptions;
console.log('param1', scriptOptions.param1);
console.log('param2', scriptOptions.param2);
doSomething(scriptOptions.param1, scriptOptions.param2);
});

Google Chrome Extension: Expose variable from injected script via chrome.tabs.executeScript to another script injected the same way

Safe approach

Use a special dedicated global variable, which you must define in a common script that runs first, for example:

chrome.tabs.executeScript({ code: 'var globalData = {foo: true}' });
chrome.tabs.executeScript({ file: 'scripts/ajaxCalls.js' });
chrome.tabs.executeScript({ file: 'scripts/frontEndDev.js' });

Then use globalData.whatever to access the data and globalData.whatever = 'anything' to set it. Since this is a standard JS object you can set a key inside even if it wasn't defined initially.

To make it even safer you can use the built-in function that converts objects into proper strings: 'var globalData = ' + JSON.stringify({ foo: true })

Unsafe approach

Share foo as window.foo = 'anything' then use it literally as foo or window.foo - both are the same, but the first one may require you to pacify your linting tool e.g. /* global foo */ in eslint.

It's unsafe in a content script because a web page can have an element with an id attribute containing the exact name of that variable, which historically creates an implicit global variable accessible both in the page context and in the isolated world of content scripts. Some pages may accidentally or intentionally break extensions by adding such an id and if they do it on the <html> element the variable will be created even for content scripts running at document_start.

The first approach was safe because it explicitly defined a global variable before running any dependent code (using a hardcoded name when defining, accessing, writing) thus overriding a possible implicit variable generated for a DOM element.

Some trivia

  • Such shared data can be used only by code that runs later in time (not simply the one that's "outside" the callback, more info)

  • All content scripts for the given page or frame run in the special "isolated world" of content scripts of this extension so they share globals like window or globalThis or any variables declared in the global scope of any script via var, let, const, class. For example, goSortParam in your code is a global variable so it's available in all content scripts directly. The isolated world is basically a standard JavaScript environment like that of any web page, but it's separate/isolated so variables (or DOM expandos) defined in one environment aren't visible in the other one. Every extension is assigned its own isolated world on every page/frame where its content scripts run.

Chrome Extension Pass Variable from popup.js to content script

You can use messaging to send the data to the injected script, but you need the tab Id.

popup.js

function FireInjectionScript() {
//Want to Pass this variable.
var updateTextTo = document.getElementById('mat_no').value.trim();
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
chrome.tabs.executeScript(tabs[0].id,{file: 'InjectionScript.js'},()=>{
chrome.tabs.sendMessage(tabs[0].id,{myVar:updateTextTo});
});
});
}

document.getElementById('caseButton').addEventListener('click', FireInjectionScript);

InjectionScript.js

chrome.runtime.onMessage.addListener(message=>{
if (message.myVar) {
//here you can access message.myVar
}
});

Communicate data from popup to content script injected by popup with executeScript()

There are three general ways to do this:

  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. Use message passing (MDN) to pass the data after your script is injected.

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: 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('"
+ JSON.stringify(updateTextTo).replace(/\\/g,'\\\\').replace(/'/g,"\\'") + "';"
}, function () {
chrome.tabs.executeScript({
file: "content_script3.js"
});
});

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.

Pass arguments to a file script executed by chrome.scripting.executeScripts? (manifest v3)

Option 1

executeScript in MV3 doesn't have code parameter so you need to rewrite it using func and set the arguments as properties of window (or its alias self), which is the same as a globally declared var for all practical purposes.

chrome.scripting.executeScript({
target: {tabId},
args: [{eleID, type, headerHeight: offsetHeight + 10}],
func: vars => Object.assign(self, vars),
}, () => {
chrome.scripting.executeScript({target: {tabId}, files: ['./executeScript.js']});
});

Option 2

Alternatively, you can make your injected file declare the functions without calling them:

// executeScript.js
function scrollToTarget(eleID, type, headerHeight = 40) {
console.log({eleID, type, headerHeight);
}

Then you'll call the function after injecting the file:

chrome.scripting.executeScript({target: {tabId}, files: ['./executeScript.js']}, () => {
chrome.scripting.executeScript({
target: {tabId},
args: [eleID, type, offsetHeight + 10],
func: (...args) => scrollToTarget(...args),
});
});


Related Topics



Leave a reply



Submit