Search code examples
javascriptfirefoxfirefox-addonfirefox-addon-sdk

Callbacks from content script in firefox Addon SDK


I'm working on a small extension with the Firefox addon-sdk that has to alter the content of DOM elements in pages. I'm using PageMod to add the content script and register some events, some of which I want to pass along a callback function to, like this :

main.js

pageMod.PageMod({
    include: "*",
    contentScriptWhen: 'ready',
    contentScriptFile: [self.data.url("my_content_script.js")],
    onAttach: function(worker) {
        worker.port.on("processElement", function(elementSrc, callback) {
            doSomeProcessingViaJsctypes();
            callback("http://someUrl/image.png");
        });
    }
});

my_content_script.js

var elements = document.getElementsByTagName("img");
var elementsLength = elements.length;
for (var i = 0; i < elementsLength; i++) 
{

    (function(obj) {
        obj.setAttribute("data-processed", "true");
        self.port.emit("processElement", obj.src, function(newSrc) {
            console.log("replaced " + obj.src);
            obj.src = newSrc;
        });
    })(elements[i]);
}

The error

TypeError: callback is not a function
Stack trace:
.onAttach/<@resource://gre/modules/XPIProvider.jsm -> file:///c:/users/sebast~1/appdata/local/temp/tmpjprtpo.mozrunner/extensions/jid1-gWyqTW27PXeXmA@jetpack/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://jid1-gwyqtw27pxexma-at-jetpack/testextension/lib/main.js:53

I can't seem to find anything on the matter on the web. I need this approach since the processing takes a bit of time and depends on a .dll file so I can't call it from the content script.

If I were to process the element and after that call a worker.port.emit() I would have to iterate through the entire tree again to identify the element and change it's src attribute. This will take a long time and would add extra loops for each img in the document.

I've thought about generating a unique class name and appending it to the element's classes and then calling getElementsByClassName(). I have not tested this but it seems to me that it would take the same amount of time as the process I described above.

Any suggestions would be appreciated.

EDIT : I have found this answer on a different question. Wladimir Palant suggests using window-utils to get the activeBrowserWindow and then iterate thorough it's content.

He also mentions that

these low-level APIs aren't guaranteed to be stable and the window-utils module isn't even fully documented

Has anything changed since then? I was wondering if you can get the same content attribute using the tabs and if you can identify the tab from which a worker sent a self.port.emit().


Solution

  • When using messaging between content-scripts and your modules (main.js), you can only pass data around that is JSON-serializable.

    Passing <img>.src should be OK, as this a string, and therefore JSON-serializable. Your code breaks because of the callback function you're trying to pass, since function is not JSON-serializable (same as whole DOM nodes are not JSON-serializable). Also, .emit and .on use only the first argument as the message payload.

    Instead of a callback, you'll have to actually emit another message back to the content script after you did your processing. And since you cannot pass DOM elements, you'll need to keep track of what DOM element belongs to what message.

    Alright, here is for example how I'd do it. First main.js:

    const self = require("sdk/self");
    const {PageMod} = require("sdk/page-mod");
    
    function processImage(src) {
      return src + " dummy";
    }
    
    PageMod({
      include: "*",
      contentScriptWhen: 'ready',
      contentScriptFile: [self.data.url("content.js")],
      onAttach: function(worker) {
        worker.port.on("processImage", function(data) {
          worker.port.emit("processedImage", {
            job: data.job,
            newSrc: processImage(data.src)
          });
        });
      }
    });
    

    In my design, each processImage message has a job associated with it (see the content script), which main.js considers opaque and just posts back verbatim with the response.

    Now, data/content.js, aka. my content script:

    var jobs = new Map();
    var jobCounter = 0;
    
    self.port.on("processedImage", function(data) {
      var img = jobs.get(data.job);
      jobs.delete(data.job);
      var newSrc = data.newSrc;
      console.log("supposed replace", img.src, "with", newSrc);
    });
    
    for (var i of document.querySelectorAll("img")) {
      var job = jobCounter++; // new job number
      jobs.set(job, i);
      self.port.emit("processImage", {
        job: job,
        src: i.src
      });
    }
    

    So essentially for each image, we will create a job number (could be an uuid or whatever instead, but incrementing a counter is good enough for our use case), and put the DOM image associated with that job number into a map to keep track of it. After that is, just post the message to main.js.

    The processedImage handler, will the receive back the job number and new source, use the job number and jobs map get back the DOM element, remove it from the map again (we don't wanna leak it stuff) and do whatever processing is required; in this example just log stuff.