8

I'm building a Google Chrome extension. The basic setup is I have a Browser action button that injects jQuery and another bit of JavaScript into the active tab when it is clicked to do it's thing.

This is my first Chrome extension, but it seems like if the user clicks the button to take action a second time the scripts will be re-injected. This is a problem because the main pages this is going to work with are all AJAX, so the page content changes significantly but the actual page URL never changes.

Is this a legitimate concern, or am I over thinking it and is there a simple way to prevent the scripts from being injected multiple times into the same page? Should I perhaps be specifying these as content scripts instead?

Xan
  • 74,770
  • 16
  • 179
  • 206
Pete
  • 248
  • 2
  • 9
  • Whether you want a content script or browser action depends on the conditions under the script should be injected. – Teepeemm Apr 22 '14 at 11:56

3 Answers3

11

It is absolutely a legitimate concern.

The easy way would be to use a mechanism similar to #ifndef include guards in C.

Define a flag that gets set once the content script gets executed, and check if it's defined before doing anything. All scripts injected from the same extension share the JS execution context and therefore global scope.

Your content script might look like this:

if (window.contentScriptInjected !== true) {
    window.contentScriptInjected = true; // global scope

    /* Do your initialization and work */
}

/* global function definitions */

The check should use an explicit true value to avoid false positives on pages that happen to have an element with id attribute that accidentally equals the variable name - browsers create an implicit global variable that points to that DOM element so for an if check it'll be truthy.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Xan
  • 74,770
  • 16
  • 179
  • 206
  • Injected content scripts share context even across tabs? Are you sure? – Pete Apr 22 '14 at 14:13
  • @Pete No, they do not. I thought you were concerned by injection several times in the same tab. – Xan Apr 22 '14 at 14:29
  • I am, but the injection happens from the global context of a browser action background.html so having a single variable to track if the injection has happened doesn't seem like it would work if there are 10 tabs for example. I'm not talking about injecting – Pete Apr 22 '14 at 14:46
  • @Pete You misunderstand. This is the **content script** code. The variable lives in the tab's context and guarantees that the script is injected only once in that tab. That is to say, it can be injected more than once but the code will only run once. – Xan Apr 22 '14 at 15:06
  • The problem with this is if you include third party libraries as well. – Maciej Krawczyk May 30 '19 at 07:34
1

Came up with another solution that is working with Manifest V3

  • inject a variable that you will later check let isMyVarSet = true
  • before injecting your script, check if this variable is true from the background script
  async function isScriptInjected(tabId) {
    let response = await chrome.scripting.executeScript({
      target: { tabId: tabId },
      func: () => typeof isMyVarSet !== "undefined" && isMyVarSet === true,
    });
    return response[0].result
  }

Inject all if isScriptInjected returns false, otherwise only inject initialization scripts

P.S. The check should use an explicit true value to avoid false positives on pages that happen to have an element with id attribute that accidentally equals the variable name - browsers create an implicit global variable that points to that DOM element so for an if check it'll be truthy.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Kallos Zsolty
  • 211
  • 3
  • 5
0

I believe the better way might be to do it on extension level. If you include third party libraries, then you would be re-including them as well, and never know what happens then.

contentScript.js

(function () {

  var Sz = Sizzle;

  //listen for events from the extension
  chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {

    if (!request._extension || request._extension !== 'my-extension-name') {
      console.log('Non extension request');
      return;
    }

    var channel = (request._channel) ? request._channel : false;

    if (channel === 'is-loaded') {

      sendResponse({isLoaded: true});
      return true;

    } else if (channel === 'example') {
      //do stuff 
    }

    return true;

  });

  //other code goes here

});

Background.js

var util = {};

util.sendMessage = function (data, cb) {

  chrome.tabs.query({active: true, currentWindow: true}, tabsQuery_cb);

  data['_extension'] = 'my-extension-name';

  function tabsQuery_cb(tabs) {

    var tab = tabs[0];

    console.log('sending message to ' + tab.id);
    console.log(data);

    //let the content script know to open the ad page
    chrome.tabs.sendMessage(tabs[0].id, data, cb);

  }

}

util.loadContentScript = function(cb) {

  var attempts = 0;


  checkIfLoaded();

   //checks if ContentScript is loaded by sending a message to the current
  function checkIfLoaded() {

    util.sendMessage({ _channel: 'is-loaded'}, sendMessage_cb);

  }

  function sendMessage_cb(response) {

    console.log('isLoadedCheck')
    console.log(response);

    if (response && response.isLoaded === true) {
      console.log('Script already loaded!');
      cb(true);
    } else {
      console.log('loading scripts');
      loadScripts();
    }

  }

  function loadScripts() {

    attempts++;
    console.log('loadScripts, attempt no. ' + attempts);

    if (attempts > 1) {
      console.log('Script couldnt be loaded');
      return cb(false);
    }

    var scripts = [
      "assets/sizzle-2.3.4.min.js",
      "app/contentScript.js"
    ];

    var i = -1;

    scriptLoaded();

    function scriptLoaded() {

      i++;

      if (i > scripts.length) {
        //all scripts have loaded at this point, check if replies with message
        checkIfLoaded();
      } else {
        console.log('Loading script ' + scripts[i]);
        chrome.tabs.executeScript(null, { file: scripts[i] }, scriptLoaded);
      }


    }

  }

}

Of course including third party libraries seems like a bad practice anyway, because it can skew with website's scripts. But what other way is there. Perhaps you could go to the extreme, and create a bootstrap content script which would check for presence of libraries and tell the extension script what exactly needs to be included. That's what I would consider on a more serious project.

Maciej Krawczyk
  • 14,825
  • 5
  • 55
  • 67
  • > Of course including third party libraries seems like a bad practice anyway, because it can skew with website's scripts. Content Scripts "work in isolated worlds" from the web page scripts, which allow them to even use different versions of the same script https://developer.chrome.com/extensions/content_scripts#isolated_world – Fernando César Sep 15 '19 at 19:09