Search code examples
javascriptgoogle-chrome-extension

How can I prevent multiple injection/execution of content script(s) in response to a single event?


I am building a chrome extension that responds to click events on a context menu.

My background script creates the context menu using the chrome.contextMenus.create api method call and sets a click handler as shown in the code below:

//event.js
function onItemClick(info, tab){
    // Inject the content script into the current page
    chrome.tabs.executeScript(null, { file: 'content.js' });

    // Perform the callback when a message is received from the content script
    chrome.runtime.onMessage.addListener(function(message){
        var url = "data:text/html;charset=utf8,";

        function append(key, value){
            var input = document.createElement('textarea');
            input.setAttribute('name', key);
            input.textContent = value;
            form.appendChild(input);
        }

        var form = document.createElement('form');
        form.method = 'POST';
        form.action = 'http://localhost/myapp/myapp.php';
        form.style.visibility = "hidden";
        append('url', message.url);
        append('text', message.selectedText);
        url = url + encodeURIComponent(form.outerHTML);
        url = url + encodeURIComponent('<script>document.forms[0].submit();</script>');
        chrome.tabs.create({url: url, active: true});
    });
}

var context = "selection";
var title = "Share in new tab";
var id = chrome.contextMenus.create({"title": title, "contexts": [context], "onclick": onItemClick});

The background script above programmatically creates a form that automatically gets submitted in a new tab. In doing so, it calls a "content script" below to get some information from the current page/tab.

//content.js
chrome.runtime.sendMessage({
    'url': window.location.href,
    'selectedText': window.getSelection().toString()
});

The problem is this. The click handler in the background script injects the "content script" into the current page multiple times (that is, every time the click handler is called). As a result of this multiple injection, each injected instance of the "content script" is executed resulting in multiple new tabs/pages being opened. The number of new tabs opened increases by one each time the context menu item is clicked suggesting the problem is indeed multiple injection and execution of the content script. How can I inject the content script only once, or at least ensure that only one "instance" of the injected scripts sends a message back to my background script?

I have tried to automatically inject the script in the manifest, but calling chrome.tabs.executeScript thereafter results in an endless creation of tabs. So, I really need to be able to inject the script on demand, but find a way to either prevent multiple injections or at least ensure only one "injection" sends a message back. Please help!


Solution

  • The solution can be achieved easily: you can create a global control variable in the content script. Check for it at the beginning, and if it is not true then set it to true and proceed. The first content script that gets executed will set the variable and prevent others from doing anything.

    By the way, I see you're adding a listener to chrome.runtime.onMessage inside another listener: that is not good practice, because it will add multiple listeners for the same event and result in executing them multiple times. You should instead declare the listener outside, sending a different message saying "do something" or "do something else".

    In the content script:

    // Warning! Comparing to a literal to ignore a DOM element with id=messageSent
    // which automatically creates an implicit global on `window`
    if (window.messageSent !== true) {
        window.messageSent = true;
    
        chrome.runtime.sendMessage({
            action: "submit the form",
            url: window.location.href,
            selectedText: window.getSelection().toString()
        });
    }
    

    In the background.js:

    chrome.runtime.onMessage.addListener(function(message){
        if (message.action == "submit the form") {
            // do what you need to submit the form
            var url = "data:text/html;charset=utf8,";
            function append(key, value){
            ...
        }
    });