Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sending message from a background script to a content script, then to a injected script

I'm trying to send messages from the background page to a content script, then send a message from that content script to an injected script. I've tried this, but it isn't working.

Here's what my code looks like.

manifest.json

{
  "manifest_version": 2,

  "name": "NAME",
  "description": ":D",
  "version": "0.0",
  "permissions": [
    "tabs","<all_urls>"
  ],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content_script.js"]
    }
  ],
  "web_accessible_resources": [
      "injected.js"
  ],
  "background":{
      "scripts":["background.js"]
  }
}

background.js

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response){});
});

content_script.js

var s = document.createElement('script');
s.src = chrome.extension.getURL('injected.js');
s.onload = function(){ 
        this.parentNode.removeChild(this);
};
(document.head||document.documentElement).appendChild(s);


chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    document.dispatchEvent(new CustomEvent('Buffer2Remote', {todo: "LOL"}));
});

injected.js

document.addEventListener('Buffer2Remote', function(e){
    alert(e.todo);
});

The message sending doesn't work from the first part, background -> content_script. Is there anything wrong with my code?

like image 371
user3653206 Avatar asked May 27 '14 17:05

user3653206


People also ask

What is the difference between content script and background script?

Background Script - Provides persistence and handles background events. Content Script - Scripts that run in isolation in the context of the web page. Injected Script - Scripts that are programmatically injected into the web page.

What is content script?

A content script is a part of your extension that runs in the context of a particular web page (as opposed to background scripts which are part of the extension, or scripts which are part of the website itself, such as those loaded using the <script> element).

What is chrome runtime sendMessage?

runtime. onMessage API functions. The chrome.runtime.sendMessage function is used to send one time messages from one part of the extension to another part. The function receives a message object which can be any JSON serializable object and an optional callback to handle the response from the other part.


1 Answers

Your script doesn't work because of how content scripts are injected.

Problem

When you (re)load your extension, contrary to what some people expect, Chrome will not inject content scripts into existing tabs that match patterns from the manifest. Only after the extension is loaded, any navigation will check the URL for matching and will inject the code.

So, the timeline:

  1. You open some tabs. No content scripts there1.
  2. You load your extension. Its top level code gets executed: it tries to pass a message to the current tab.
  3. Since there can be no listener there yet, it fails. (Which is probably the chrome://extensions/ page and you can't inject there anyway)
  4. If, afterwards, you try to navigate/open a new tab, the listener gets injected, but your top level code no longer gets executed.

1 - This also happens if you reload your extension. If there was a content script injected, it continues to handle its events / doesn't get unloaded, but can no longer communicate with the extension. (for details, see addendum at the end)

Solutions

Solution 1: you can first ask the tab you're sending a message to whether it's ready, and upon silence inject the script programmatically. Consider:

// Background
function ensureSendMessage(tabId, message, callback){
  chrome.tabs.sendMessage(tabId, {ping: true}, function(response){
    if(response && response.pong) { // Content script ready
      chrome.tabs.sendMessage(tabId, message, callback);
    } else { // No listener on the other end
      chrome.tabs.executeScript(tabId, {file: "content_script.js"}, function(){
        if(chrome.runtime.lastError) {
          console.error(chrome.runtime.lastError);
          throw Error("Unable to inject script into tab " + tabId);
        }
        // OK, now it's injected and ready
        chrome.tabs.sendMessage(tabId, message, callback);
      });
    }
  });
}

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  ensureSendMessage(tabs[0].id, {greeting: "hello"});
});

and

// Content script
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if(request.ping) { sendResponse({pong: true}); return; }
  /* Content script action */
});

Solution 2: always inject a script, but make sure it only executes once.

// Background
function ensureSendMessage(tabId, message, callback){
  chrome.tabs.executeScript(tabId, {file: "content_script.js"}, function(){
    if(chrome.runtime.lastError) {
      console.error(chrome.runtime.lastError);
      throw Error("Unable to inject script into tab " + tabId);
    }
    // OK, now it's injected and ready
    chrome.tabs.sendMessage(tabId, message, callback);
  });
}

and

// Content script
var injected;

if(!injected){
  injected = true;
  /* your toplevel code */
}

This is simpler, but has complications on extension reload. After an extension is reloaded, the old script is still there1 but it's not "your" context anymore - so injected will be undefined. Beware of side effects of potentially executing your script twice.


Solution 3: just indiscriminately inject your content script(s) on initialization. This is only safe to do if it's safe to run the same content script twice, or run it after the page is fully loaded.

chrome.tabs.query({}, function(tabs) {
  for(var i in tabs) {
    // Filter by url if needed; that would require "tabs" permission
    // Note that injection will simply fail for tabs that you don't have permissions for
    chrome.tabs.executeScript(tabs[i].id, {file: "content_script.js"}, function() {
      // Now you can use normal messaging
    });
  }
}); 

I also suspect that you want it to run on some action, and not on extension load. For example, you can employ a Browser Action and wrap your code in a chrome.browserAction.onClicked listener.


Addendum on orphaned content scripts

When an extension gets reloaded, one would expect Chrome to clean up all content scripts. But apparently this is not the case; content scripts' listeners are not disabled. However, any messaging with parent extension will fail. This should probably be considered a bug and may at some point be fixed. I'm going to call this state "orphaned"

This is not a problem in either of two cases:

  1. Content script has no listeners for events on the page (e.g. only executes once, or only listens to messages from background)
  2. Content script does not do anything with the page, and only messages the background about events.

However, if that's not the case, you've got a problem: the content script might be doing something but failing or interfering with another, non-orphaned instance of itself.

A solution to this would be:

  1. Keep track of all event listeners that can be triggered by the page
  2. Before acting on those events, send a "heartbeat" message to background. 3a. If the background responds, we're good and should execute the action. 3b. If the message passing fails, we're orphaned and should desist; ignore the event and deregister all listeners.

Code, content script:

function heartbeat(success, failure) {
  chrome.runtime.sendMessage({heartbeat: true}, function(reply){
    if(chrome.runtime.lastError){
      failure();
    } else {
      success();
    }
  });
}

function handler() {
  heartbeat(
    function(){ // hearbeat success
      /* Do stuff */
    }, 
    function(){ // hearbeat failure
      someEvent.removeListener(handler);
      console.log("Goodbye, cruel world!");
    }
  );
}
someEvent.addListener(handler);

Background script:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if(request.heartbeat) { sendResponse(request); return; }
  /* ... */
});    
like image 127
Xan Avatar answered Sep 28 '22 03:09

Xan