Tom Ashworth
Tom Ashworth

Reputation: 2665

Chrome extension content script re-injection after upgrade or install

After the Chrome extension I'm working on is installed, or upgraded, the content scripts (specified in the manifest) are not re-injected so a page refresh is required to make the extension work. Is there a way to force the scripts to be injected again?

I believe I could inject them again programmatically by removing them from the manifest and then handling which pages to inject in the background page, but this is not a good solution.

I don't want to automatically refresh the user's tabs because that could lose some of their data. Safari automatically refreshes all pages when you install or upgrade an extension.

Upvotes: 74

Views: 40198

Answers (7)

Tom Ashworth
Tom Ashworth

Reputation: 2665

There's a way to allow a content script heavy extension to continue functioning after an upgrade, and to make it work immediately upon installation.

Install/upgrade

The install method is to simply iterate through all tabs in all windows, and inject some scripts programmatically into tabs with matching URLs.

ManifestV3

manifest.json:

"background": {"service_worker": "background.js"},
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],

These host_permissions should be the same as the content script's matches.

background.js:

chrome.runtime.onInstalled.addListener(async () => {
  for (const cs of chrome.runtime.getManifest().content_scripts) {
    for (const tab of await chrome.tabs.query({url: cs.matches})) {
      if (tab.url.match(/(chrome|chrome-extension):\/\//gi)) {
        continue;
      }
      const target = {tabId: tab.id, allFrames: cs.all_frames};
      if (cs.js[0]) chrome.scripting.executeScript({
        files: cs.js,
        injectImmediately: cs.run_at === 'document_start',
        world: cs.world, // requires Chrome 111+
        target,
      });
      if (cs.css[0]) chrome.scripting.insertCSS({
        files: cs.css,
        origin: cs.origin,
        target,
      });
    }
  }
});

This is a simplified example that doesn't handle frames. You can use getAllFrames API and match the URLs yourself, see the documentation for matching patterns.

Caveats & Notes

  • If you still have old tabs open you may get an error: Cannot access contents of the page. Extension manifest must request permission to access the respective host. You will need to refresh those tabs so that they now have the new extension code that will auto-reload them.
  • You may now get a new error or still get Error: Extension context invalidated. If you are still receiving this you will need to remove any old event handlers on the page for the content script see: How to remove orphaned script after Chrome extension update?

ManifestV2

Obviously, you have to do it in a background page or event page script declared in manifest.json:

"background": {
    "scripts": ["background.js"]
},

background.js:

// Add a `manifest` property to the `chrome` object.
chrome.manifest = chrome.runtime.getManifest();

var injectIntoTab = function (tab) {
    // You could iterate through the content scripts here
    var scripts = chrome.manifest.content_scripts[0].js;
    var i = 0, s = scripts.length;
    for( ; i < s; i++ ) {
        chrome.tabs.executeScript(tab.id, {
            file: scripts[i]
        });
    }
}

// Get all windows
chrome.windows.getAll({
    populate: true
}, function (windows) {
    var i = 0, w = windows.length, currentWindow;
    for( ; i < w; i++ ) {
        currentWindow = windows[i];
        var j = 0, t = currentWindow.tabs.length, currentTab;
        for( ; j < t; j++ ) {
            currentTab = currentWindow.tabs[j];
            // Skip chrome:// and https:// pages
            if( ! currentTab.url.match(/(chrome|https):\/\//gi) ) {
                injectIntoTab(currentTab);
            }
        }
    }
});

Historical trivia

In ancient Chrome 26 and earlier content scripts could restore connection to the background script. It was fixed http://crbug.com/168263 in 2013. You can see an example of this trick in the earlier revisions of this answer.

Upvotes: 78

Ishwar Rimal
Ishwar Rimal

Reputation: 1101

I was trying to do something similar, but the requirement itself was wrong in my case.
I thought I wanted to pass a message to the content script immediately after installing the app, but in actual what I wanted was to pass message once the page is loaded.
During installation, there is no reason to access the content script.
To achieve this, this is what I did:

  1. Adding webNavigation to the permission field in manifest.json.
  2. listening for webNavigation.complete event in the background script:
chrome.webNavigation.onCompleted.addListener();
  1. Pass message to the content script from the callback:
chrome.webNavigation.onCompleted.addListener(function () {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
      // Send a message to the content script in the active tab to create the modal
      chrome.tabs.sendMessage(tabs[0].id, { message: "mymessage" });
    });
  },
  { url: [{ schemes: ["http", "https"] }] })
  1. Add a listener in the content script

Upvotes: 0

theicfire
theicfire

Reputation: 3067

Due to https://bugs.chromium.org/p/chromium/issues/detail?id=168263, the connection between your content script and background script is severed. As others have mentioned, one way to get around this issue is by reinjecting a content script. A rough overview is detailed in this StackOverflow answer.

The main tricky part is that it's necessary to "destruct" your current content script before injecting a new content script. Destructing can be really tricky, so one way to reduce the amount of state you must destruct is by making a small reinjectable script, that talks to your main content script over the DOM.

Upvotes: 1

Prateek Jha
Prateek Jha

Reputation: 135

Try this in your background script. Many of the old methods have been deprecated now, so I have refactored the code. For my use I'm only installing single content_script file. If need you can iterate over chrome.runtime.getManifest().content_scripts array to get all .js files.

chrome.runtime.onInstalled.addListener(installScript);

function installScript(details){
    // console.log('Installing content script in all tabs.');
    let params = {
        currentWindow: true
    };
    chrome.tabs.query(params, function gotTabs(tabs){
        let contentjsFile = chrome.runtime.getManifest().content_scripts[0].js[0];
        for (let index = 0; index < tabs.length; index++) {
            chrome.tabs.executeScript(tabs[index].id, {
                file: contentjsFile
            },
            result => {
                const lastErr = chrome.runtime.lastError;
                if (lastErr) {
                    console.error('tab: ' + tabs[index].id + ' lastError: ' + JSON.stringify(lastErr));
                }
            })
        }
    });    
}

Upvotes: 9

quantdaddy
quantdaddy

Reputation: 1474

Chrome has added a method to listen for the install or upgrade event of the extension. One can re-inject the content script when such an event occur. https://developers.chrome.com/extensions/runtime#event-onInstalled

Upvotes: 2

saroyanm
saroyanm

Reputation: 758

The only way to force a content script to be injected without refreshing the page is via programatic injection.

You can get all tabs and inject code into them using the chrome tabs API. For example you can store a manifest version in local storage and every time check if the manifest version is old one (in background page), if so you can get all active tabs and inject your code programmatically, or any other solution that will make you sure that the extension is updated.

Get all tabs using:
chrome.tabs.query

and inject your code into all pages
chrome.tabs.executeScript(tabId, {file: "content_script.js"});

Upvotes: 32

Alireza
Alireza

Reputation: 5673

can't you add ?ver=2.10 at the end of css or js you upgraded?

"content_scripts": [ {
      "css": [ "css/cs.css?ver=2.10" ],
      "js": [ "js/contentScript.js?ver=2.10" ],
      "matches": [ "http://*/*", "https://*/*" ],
      "run_at": "document_end"
   } ],

Upvotes: -14

Related Questions