Adding external script in an extension

I’m not sure you understand, when you call “unregister” it will kill the script you passed as string in the “code” field. So if you put there your target remote script directly, it will kill that script and all the handlers. Only thing that will remain is whatever DOM changes you do, but that’s up to you to take care of, there is no way to undo those automatically.

@juraj.masiar I think I need to simplify my issue a bit to find out whether this option is feasible for my use-case.

This is a simplified view of my set-up:

/* Inject Function */
async function insertBootstrap(e) {
	var host = "nexus.ensighten.com",
		clientId = "client_id",
		space = "test_space";

	if(!window.insertBootstrapReg){
		window.insertBootstrapReg = await browser.userScripts.register({
			matches: ["<all_urls>"],
			js: [{ code: 'link=document.createElement(\'script\'); link.src=\'https://' + host + '/' + clientId + "/" + space + '/Bootstrap.js\';document.getElementsByTagName(\'head\')[0].appendChild(link);' }]
		});
	}
}

/* Remove Inject */
async function removeInsert() {
	if(window.insertBootstrapReg){
		window.insertBootstrapReg.unregister();
	}
}

The top function is called whenever the user selects the inject functionality of the extension.

The remove function is called whenever the inject option is turned off or changed to another rival feature (like re-write an existing on-page Bootstrap script or block existing Bootstrap).

If you mocked up something similar, you will see what is happening to me too. Essentially once the script is unregistered, then any redeclaration of the a userScript using the same variable name will not work. So essentially I would need a way to create a brand new userScript variable for each time I need to toggle the feature on which doesn’t seem very maintainable.

Given this summary, how would you suggest to move forward with this or another solution to allow our users to toggle the feature on and off on demand without any issues?

Let’s see. Piece of cake :slight_smile:
First - the re-declaration is not a problem at all.
Look:

const x = 1;
const x = 2; // problem! Redeclaration of "x"
(() => {
  const x = 1;
})();
(() => {
  const x = 2;  // not a problem anymore, both lives in a separate anonymous functions
})();

So just wrap your whole code into IIFE and you can execute again and again.

Second - the code you pasted is only injecting remote script via script tag - don’t do that, download the bootrap.js with a fetch() and inject only that.

Alternative: blocks!

{
  const x = 1;
}
{
  const x = 2;
}
1 Like

Blocks are great of course, but it may not help if you use “var”, which is pretty probable in this case :smiley:. Not to mention obviously missing strict mode. So it’s better be safe and use IIFE.

But thanks for the link! I’ve just learned there are Labelled blocks!!!
Man! I’m so happy when learn something new! And I’m totally gonna use somewhere.

1 Like

@juraj.masiar Still no luck. Is it possible to edit the code snippet I sent to incorporate the changes that you would recommend?

As I declare the variable inside a function (as it needs to be because it is fucntionality only needed when the user selects to ‘inject’), the variable scoping will be off when refereced by the ‘remove’ fucntion to unregister.

Oh man :slight_smile:, please don’t ask me to edit your code. I can give you guidance but you need to do the rest yourself.

Let’s try something simple. The code I’ve wrote you before:

const code = await (await fetch('https://your_bootstrap.js')).text();
await browser.userScripts.register({js: [{code: code, ...}]});

Then the escaping we are talking about has to be done in your remote script - the “bootstrap.js”.
For example if your script looks like this:

// my remote script
console.log('hello world');

Then edit it so it looks like this:

(() => {
// my remote script
console.log('hello world');
})();

Then when you call unregister, (the function returned by the browser.userScripts.register() function), it will kill the script.
The End!
No need to inject any link nodes, script nodes and other global variables :slight_smile:.

@juraj.masiar I am sorry to ask, I am just really struggling to translate all the useful links and snippets to something that is fit for purpose on our addon.

From your reply it seems there could be a catch here that makes it not possible. You mentioned putting the IIFE syntax around code in the Bootstrap.js file. As we have thousands of customer Bootstrap files that could be injected in here (as well as hundreds of sandbox accounts) we will have no control with adding addiitonal content to assist the addon.

All the code and mechanics need to sit within the addon scripts or it will not be feasible for us. Can you tailor some code examples using my code I sent (that is only a light static version of the real thing anyway so not going to copy and paste as is) so I can use this feature to inject a script dynamically?

You are downloading the script as text, so all you have to do is prefix it with “(() => {” and append “})();”.

const code = await (await fetch('https://your_bootstrap.js')).text();
await browser.userScripts.register({js: [{code: `
  (() => {
    ${code}
  })();
`, ...}]});

See? Piece of cake. Also did you noticed I’ve used Template strings instead of string concatenation? :slight_smile:

@juraj.masiar That all seems fine in general, but I have to have the register and unregister in two different fucntions as shown in my code example. So using a const or var would not work and I cannot declare it outside of the inject function as this will have all the dynamic data the user has entered into the inject fields. So I will have the same scoping issue that drove me to trying to use window variables.

Also, a user may enter many different URL’s for the Bootstrap.js file over the lifecycle of the addon so it needs to be set-up dynamically. I cannot see from your current examples how I can declare the URL of the script needed in one fucntion and then unregister it is a separate function, but also be able to re-call the inject script later in the user journey and ensure we only fire the new inject declaration and not the one from the previous attempt.

I hope that makes more sense to my limitation that are out of my control here with my internal remit for this addon.

Anything further you can add to help me out (I really do appreciate the effort you have and are going through to try and help me so far :slight_smile: )

I see that you lack some basic programming knowledge. :slight_smile:
I would highly advice you to go through the WHOLE course of JavaScript at MDN:
https://developer.mozilla.org/en-US/docs/Web/JavaScript

It’s very long but also very well written and it covers everything you need to know to finish up your task.
Almost all of my JavaScript knowledge comes from MDN.

Also MDN is full of examples, for example how to use unregister:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts/RegisteredContentScript/unregister#examples
(although it’s using “var” and not “let”)

Regarding the dynamic part of the URL - I really don’t see what is the problem here, you are already dynamically composing URL when building the script tag. But I have a feeling someone else wrote that piece of code :slight_smile:.

@juraj.masiar I understand it must be frustrating for you all this back and forth. But my JS knowledge is fine in general, it is just that the tips you have sent do not fully work for my use-case at the moment. I am sure if we did a screenshare that we would probably find the tiny nuance that I am missing specific for my use-case.

With your recent updates I can definitely see it firing (though the fetch version doesn’t work, no errors, so I stuck with the script HTML method). But when you unregister and then refresh the page, the script still comes through, so the unregister stop a new one coming through, but doesn’t remove the old ones previously injected.

You did mention before that some things in the DOM cannot be removed. So that leave us with trying to get the fetch version working I guess. This is what I have at the moment (light example version again):

/* Inject Bootstrap */
var insertBootstrapReg = null;
async function insertBootstrap(e) {
	var host = "nexus.ensighten.com",
		clientId = "client_id",
		space = "test_space";
	const insertBootstrapFetch = await (await fetch('https://' + host + '/' + clientId + space + '/Bootstrap.js')).text();
	insertBootstrapReg = await browser.userScripts.register({
			matches: ["<all_urls>"],
			js: [{ code: `(() => { ${insertBootstrapFetch} })();` }]
		});
	}
}
/* Remove Inject */
function removeInsert() {
	if (insertBootstrapReg) {
		insertBootstrapReg.unregister();
		insertBootstrapReg = null;
	}
}

I get no errors at all, so we can rule out syntax being the cuplrit, maybe you can see what could be causing it not to work?

This version looks ok.
Just debug it or use console log to verify that the downloaded script code looks fine. It’s pretty straightforward so there is not much room for bugs, right?

@juraj.masiar I can see the fetch version is working now, but I get the same issue when unregistering i.e. the call is still made on the next page load even when it is turned off in the addon.

I ran console logs and it definitely doesn’t accidentally trigger when it shouldn’t o teh background.js side. I think the final piece of the puzzle here is finding a way to ensure the parent page removes these scripts when we unregister them. Do you know a way to send a message from the background.js to the parent page to remove this history of user scripts?

Hello, tell me what is the API please? thank you

Could you try some simple experiment where you just call register, unregister and again register? With some 1s delay after each operation.

const r = await browser.userScripts.register({js: [{code: code, ...}]});
await new Promise(resolve => setTimeout(resolve, 1000));
r.unregister();
await new Promise(resolve => setTimeout(resolve, 1000));
await browser.userScripts.register({js: [{code: code, ...}]});

Also, as you can see, the API is pretty limited and straightforward so there is no “remove this history of user scripts”. You can reload the tab using “tabs.reload” to clear injected scripts. But again, the unregister method should do that without reload the tab.

What you do mean? There is a link for the documentation plus this whole thread is full of examples of this API :smiley:.
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/register

@juraj.masiar I ran this:

async function testBootstrap(e) {
	const r = await browser.userScripts.register({matches: ["<all_urls>"], js: [{ code: `console.log("Test Register 1");` }] });
	await new Promise(resolve => setTimeout(resolve, 1000));
	r.unregister();
	await new Promise(resolve => setTimeout(resolve, 1000));
	await browser.userScripts.register({matches: ["<all_urls>"], js: [{ code: `console.log("Test Register 2");` }] });
};
testBootstrap();

I only saw "Test Register 2" fire in the parent page’s console. Does that explain anything for your experiment?

I think there must be a caching issue with the scripts once they are sent to the parent page that we can’t undo (though reloading the local addon clears it all).

Seeing as the script stays even after a refresh or navigating to another page, I don’t think reloading the tab would work anyway.

What I meant was to try it with some of your code, to test it with an easy test case. If you only console log inside, it doesn’t test anything :smiley: because the script is not running anymore one second later.

Also the “userScripts.register” API will “register” the script for the specific matching URL, so reloading the page won’t help because the script will be re-executed every time you enter the page.

The page reload can help only AFTER you call unregister. But then again, it all depends on the script you are registering - if it doesn’t modify DOM, then it’s all ok.

You should start with a proof of concept, something easy to test and reason about. That’s what the experiment is about :slight_smile:.

@juraj.masiar I am not sure what you are suggesting. I have already proved the script we can deploy from our addon still fires on the next page load even after the unregister event fires. The only way to stop the firing (and duplication if you turn the inject functionality back on after a first try) for the script is to manaually reload the temporary addon (which is not an option once the extension is live).

I sense that you are not sure what else to try here, as you said; its a simple API and shouldn’t do this. Would you entertain sparing a few minutes today or tomorrow to go over this on a webex to find out once and for all what the issue as and what options we have?