javascriptgoogle-chrome-extensioncontent-script

Inject content script only once in a webpage


I am trying to inject content script on context menu click in an extension manifest version 3. I need to check if it is already injected or not. If it is not injected , inject the content script. This condition has to be satisfied. Can anyone help me with this?

We can use

ALREADY_INJECTED_FLAG

but this can be checked only in the content script, so this approach will not work as expected.

payload.js(content script)

function extract() {
    
    htmlInnerText = document.documentElement.innerText;
    url_exp = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi;
    regex =  new RegExp(url_exp)
    list_url = htmlInnerText.match(url_exp)

    ip_exp = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
    list_ip = htmlInnerText.match(ip_exp)

    hash_exp = /\b[A-Fa-f0-9]{32}\b|\b[A-Fa-f0-9]{40}\b|\b[A-Fa-f0-9]{64}\b/g
    list_hash = htmlInnerText.match(hash_exp)

    chrome.storage.local.set({ list_url: list_url, list_ip: list_ip, list_hash: list_hash });

}

chrome.runtime.sendMessage( extract());

background.js

genericOnClick = async () => {

    // Inject the payload.js script into the current tab after the backdround has loaded
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
        chrome.scripting.executeScript({
            target: { tabId: tabs[0].id },
            files: ["payload.js"]
        },() => chrome.runtime.lastError);
    });
    

    // Listen to messages from the payload.js script and create output.
    chrome.runtime.onMessage.addListener(async (message) => {
    
        chrome.storage.local.get("list_url", function (data) {
            if (typeof data.list_url != "undefined") {
                urls = data.list_url
            }
        });
        chrome.storage.local.get("list_ip", function (data) {
            if (typeof data.list_ip != "undefined") {
                ips = data.list_ip
            }
        });
        chrome.storage.local.get("list_hash", function (data) {
            if (typeof data.list_hash != "undefined") {
                hashes = data.list_hash;
            }
        });

        
        if ( hashes.length>0 || urls.length>0 || ips.length>0 ){
            chrome.windows.create({url: "output.html", type: "popup", height:1000, width:1000});
        }
    });
}

Solution

  • on my first context menu click I get the output html once. Second time I click, I get the output html twice likewise.

    This behavior is caused by a combination of two factors.

    First factor

    You're calling chrome.runtime.onMessage.addListener() inside genericOnClick(). So every time the user clicks the context menu item, the code adds a new onMessage listener. That wouldn't be a problem if you passed a named function to chrome.runtime.onMessage.addListener(), because a named function can only be registered once for an event.

    function on_message(message, sender, sendResponse) {
        console.log("bg.on_message");
        sendResponse("from bg");
    }
    
    chrome.runtime.onMessage.addListener(on_message);
    

    Second factor

    But you're not registering a named function as the onMessage handler. You're registering an anonymous function. Every click on the context menu item creates and registers a new anonymous function. So after the Nth click on the context menu item, there will be N different onMessage handlers, and each one will open a new window.

    Solution

    1. Define the onMessage handler as a named function, as shown above.
    2. Call chrome.runtime.onMessage.addListener() outside of a function.

    You don't have to do both 1 and 2. Doing either will solve your problem. But I recommend doing both, because it's cleaner.