Chrome Extension: Content Script -> Dynamic Function - google-chrome-extension

I would like to call a dynamic function on a Content Script (Chrome Extension). But the common way doesn't work:
chrome.extension.onRequest.addListener(function cs_listener(request, sender, sendResponse) {
[request.action]();
}
request.action is blah. Where function blah() is a....and now it comes...a function!
Error thrown:
Error in event handler for 'undefined': TypeError: object is not a function
Someone got over this? I really don't like to make a switch for every action I need.

You have to use
window[request.action]();
as
[request.action]();
creates an array containing request.action, and tries to call that, which results in the error. window[request.action](); gets the property named request.action from window and calls that.
You also might want to check if the property is defined first:
if(typeof window[request.action] == "function")
window[request.action]();

Another way would be just calling that function from a background page, without sending a request:
chrome.tabs.executeScript(null, {code: "dynamic_function_name()"});

Related

Error in invocation of runtime.sendMessage (Chrome API)

The goal is to send a message to a content script from a background script after a URL is changed.
Here is my function:
chrome.tabs.onUpdated.addListener(
function (tabId, changeInfo, tab){
if(changeInfo.url && changeInfo.url.includes('https://example')){
chrome.runtime.sendMessage(tabId, {warn: 'message'}, function(resp){})
}
});
But I get an error: Error, in event handler: TypeError: Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function callback): Error at parameter 'options': Unexpected property: 'warn'.
I tried to code according to documentation, but can't find the error
I was getting a similar error until I did something like
chrome.tabs.sendMessage(tabId, {warn: 'message'});
Note: tabs, not runtime method.
I had to leave out the message parameter. Not sure about callback.
You are using the API wrong. sendMessage should receive two parameters. The first one can be an object and the second one would be a callback with the response. This way:
chrome.runtime.sendMessage(
{
message: "this is a message",
},
(response) => {
// ... do something with the response
}
);

Check if a function exists inside another function in nodejs

Propose the following situation:
function functionExists(functionName) {
if (typeof window[functionName] == 'function') console.log("It's a function");
}
What would be an equivalent function in nodejs for functionExists where there is no global window variable?
CONCRETE SITUATION:
My concrete situation uses webpack instead of nodejs, but basically the problem is the same. I could use window here, but it would be too complicated to implement everything cleanly, and it isn't advised by webpack to mitigate things out to the window global variable.
Basically, I have a PHP backend, which generates a html <form> adding some options to it via a data attribute. When the page is loaded, my javascript initializes this <form> and gives it a bunch of functionalities (like validation for example). Another thing javascript does with this form, is that it parses the data attribute of it, and instead of the normal page reload submit, it changes the form so it is being submited over an ajax request to the server.
When this submit happens, it is set up, that the button and the whole form gets disabled, until my Ajax script sends back a response. How this is done, is that I have a Project_Form class, which when it is initialized, attaches itself to the jQuery submit event, stops the basic submit event, and runs an inner function which sends an ajax request to an api method. The ajax request is set up, that when a response is received, the same instantiated class will receive this response, so I can continue working with it.
When the form receives the response, it must do something with it. In the most basic situation, it must show a success message to the user, but there are some more complex situation, where for example, it has to make a page redirect (for example a login form). Right now, it is set up, that as a default, it will show a message, but when I define this form in PHP, I have the option to "hijack" this default behaviour, and instead of it, send the ajax response to a custom function, which will resolve the situation specifically.
When I am rendering the form in PHP, I already know where the form should send a success response (to which javascript function), but I can only provide this information to javascript, via a string. So my Project_Form class, should fetch this string, and should try to fetch a function from it which it will use. This is where my problem is coming from.
Unless you specifically KNOW that this is a global function (which is almost never the case in nodejs), functions by default in nodejs are scoped to the module and there is NO way to look them up by string name like you did with the window object in the browser, just like there is no way to look up local variables by name inside a function in Javascript.
In general, don't pass functions by string name. Or, if you have to, then you need to create a lookup table that you can check the function name against.
I'd suggest you explain the real problem you're trying to solve here because passing the function by string name is not how you would generally want to do things.
There is a bit of a hack using eval() that can see if a string represents a function name that is in scope:
// Warning, you must know that the argument f (if it is a string) does not
// contain harmful Javascript code because it will be used with eval()
function isFunction(f) {
// if already a function reference
if (typeof f === "function") {
return true;
// see if string represents a function name somewhere in scope
} else if (typeof f === "string") {
try {
return eval(`typeof ${f} === "function"`);
} catch(e) {
return false;
}
} else {
return false;
}
}
Note: This tests to see if the function is in the scope of the isFunction() function. If you want to test if it's in your current scope, then you need to do the:
eval(`typeof ${f} === "function"`)
inline in your current scope so it runs in the scope you want to do the lookup from.
To ever consider using this, you will HAVE to know that the source of your string is safe and cannot contain harmful code. But, as I said earlier, it's better to design your program differently so you aren't referring to functions by their string name.
And, here's a runnable snippet that shows it in action (also works in a node.js module):
function test() {
console.log("in test");
}
function isFunction(f) {
// if already a function reference
if (typeof f === "function") {
return true;
// see if string represents a function name somewhere in scope
} else if (typeof f === "string") {
try {
return eval(`typeof ${f} === "function"`);
} catch(e) {
return false;
}
} else {
return false;
}
}
console.log(isFunction("test")); // true
console.log(isFunction(test)); // true
console.log(isFunction("notAFunction")); // false
More added after question edit
If you only have the function name as a string and the function that it points to is not a property of some known object, then the only way I know of to turn that string into a function reference is with eval().
You could directly execute it with eval() such as eval(functionName + "()") or you could get a reference to the function with eval("let fn = " + functionName) and then use the newly defined fn variable to call the function.
If you control the various functions that could be referenced (because they're your Javascript), then you can make all those functions be a property of a known object in your Javsacript:
const functionDispatcher = {
function1,
function2,
function3,
function4
}
Then, instead of using eval(), you can reference them off the functionDispatcher object like you would have referenced before with window (except this isn't a global) as in:
functionDispatcher[someFunctionName]();
This would be a preferred option over using eval() since there is less risk of insertion of random code via an unsafe string.
In node.js you can achieve this like:
function functionExists(functionName) {
if(functionName && typeof functionName === "function")
console.log("It is a function");
}
Hope this works for you.

How to pass object argument to browser.execute method?

I am trying to use WebdriverIO's execute method to pass a browser object like this:
describe('reference', () => {
it('test browser', () => {
browser.execute(function(){
console.log('BROWSER:', browser);
});
});
});
But there is an error:
Failed: unknown error: browser is not defined
How can I pass my browser object to a custom function?
Any code inside of execute is run inside the browser itself, meaning that the browser object isn't available.
You can pass the browser object in, but because it won't have access to all the rest of WebdriverIO, I'm guessing it won't work like you're hoping. Regardless, here's the code for that:
browser.execute(function(wdioBrowser){
console.log('BROWSER:', wdioBrowser);
}, browser);
You do have access to the normal DOM though. So if you want to find an element, you can do:
browser.execute(function(){
console.log('Element:', document.querySelector('.my-selector'));
});
If you're looking for more details, I have an 8 minute video on it in my WebdriverIO course (#23 in the list).

Chrome Extenion - chrome.tabs.executescript - how to pass a variable in the code parameter [duplicate]

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

Message passing with chrome.downloads

I am working on a Chrome extension. I have a content script and an event page. From the content script, I send a message using chrome.runtime.sendMessage() to the event page. On the event page, I use onMessage event listener to send back a reponse -- however, I would like to send this reponse AFTER chrome has detected that a file has started downloading.
contentScript.js
window.location.href = download_link; //redirecting to download a file
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
eventPage.js
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
chrome.downloads.onCreated.addListener(function(DownloadItem downloadItem) {
sendResponse({farewell: "goodbye"});
return true;
});
});
Now, I haven't tried chrome.downloads.onCreated listener before, but I'm assuming this is the correct syntax. However, the code is not working, and the console is returning this error:
Error in event handler for (unknown): Cannot read property 'farewell' of undefined
Stack trace: TypeError: Cannot read property 'farewell' of undefined
at chrome-extension://dlkbhmbjncfpnmfgmpbmdfjocjbflmbj/ytmp3.js:59:31
at disconnectListener (extensions::messaging:335:9)
at Function.target.(anonymous function) (extensions::SafeBuiltins:19:14)
at EventImpl.dispatchToListener (extensions::event_bindings:395:22)
at Function.target.(anonymous function) (extensions::SafeBuiltins:19:14)
at Event.publicClass.(anonymous function) [as dispatchToListener] (extensions::utils:65:26)
at EventImpl.dispatch_ (extensions::event_bindings:378:35)
at EventImpl.dispatch (extensions::event_bindings:401:17)
at Function.target.(anonymous function) (extensions::SafeBuiltins:19:14)
at Event.publicClass.(anonymous function) [as dispatch] (extensions::utils:65:26)
I have tried this without the chrome.downloads.onCreated listener and it works, the response is fetched by the content script. I read online that you need to add return true; in order to make it work, but it's not working for me. I suspect it's because of the second event listener, which enters a new scope meaning that sendResponse cannot be called from there -- if that's the case, how do I call the sendResponse function?
You return true; from the wrong place.
The purpose of that return is to say "I have not called sendResponse yet, but I'm going to".
However, you are returning it from inside the onCreated callback - it is too late by then, as right after the listener is added your original onMessage handler terminates. You just have to move the line:
chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) {
chrome.downloads.onCreated.addListener( function(DownloadItem downloadItem) {
sendResponse({farewell: "goodbye"});
});
return true;
});

Resources