Porting the Library Detector

Add-ons using the techniques described in this document are considered a legacy technology in Firefox. Don't use these techniques to develop new add-ons. Use WebExtensions instead. If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions.

From Firefox 53 onwards, no new legacy add-ons will be accepted on addons.mozilla.org (AMO).

From Firefox 57 onwards, WebExtensions will be the only supported extension type, and Firefox will not load other types.

Even before Firefox 57, changes coming up in the Firefox platform will break many legacy extensions. These changes include multiprocess Firefox (e10s), sandboxing, and multiple content processes. Legacy extensions that are affected by these changes should migrate to WebExtensions if they can. See the "Compatibility Milestones" document for more.

A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.

This example walks through the process of porting a XUL-based add-on to the SDK. It's a very simple add-on and a good candidate for porting because there are suitable SDK APIs for all its features.

The add-on is Paul Bakaus's Library Detector.

The Library Detector tells you which JavaScript frameworks the current web page is using. It does this by checking whether particular objects that those libraries add to the global window object are defined. For example, if window.jQuery is defined, then the page has loaded jQuery.

For each library that it finds, the library detector adds an icon representing that library to the status bar. It adds a tooltip to each icon, which contains the library name and version.

You can browse and run the ported version in the SDK's examples directory.

How the Library Detector Works

All the work is done inside a single file, librarydetector.xul This contains:

  • a XUL overlay
  • a script

The XUL overlay adds a box element to the browser's status bar:

The script does everything else.

The bulk of the script is an array of test objects, one for each library. Each test object contains a function called test(): if the function finds the library, it defines various additional properties for the test object, such as a version property containing the library version. Each test also contains a chrome:// URL pointing to the icon associated with its library.

The script listens to gBrowser's DOMContentLoaded event. When this is triggered, the testLibraries() function builds an array of libraries by iterating through the tests and adding an entry for each library which passes.

Once the list is built, the switchLibraries() function constructs a XUL statusbarpanel element for each library it found, populates it with the icon at the corresponding chrome:// URL, and adds it to the box.

Finally, it listens to gBrowser's TabSelect event, to update the contents of the box for that window.

Content Script Separation

The test objects in the original script need access to the DOM window object, so in the SDK port, they need to run in a content script. In fact, they need access to the un-proxied DOM window, so they can see the objects added by libraries, so we’ll need to use the experimental unsafeWindow.

The main add-on script, main.js, will use a page-mod to inject the content script into every new page.

The content script, which we'll call library-detector.js, will keep most of the logic of the test functions intact. However, instead of maintaining its own state by listening for gBrowser events and updating the user interface, the content script will just run when it's loaded, collect the array of library names, and post it back to main.js:

function testLibraries() {
  var win = unsafeWindow;
  var libraryList = [];
  for(var i in LD_tests) {
    var passed = LD_tests[i].test(win);
    if (passed) {
      var libraryInfo = {
        name: i,
        version: passed.version
      };
      libraryList.push(libraryInfo);
    }
  }
  self.postMessage(libraryList);
}
testLibraries();

main.js responds to that message by fetching the tab corresponding to that worker using worker.tab, and adding the array of library names to that tab's libraries property:

pageMod.PageMod({
  include: "*",
  contentScriptWhen: 'end',
  contentScriptFile: (data.url('library-detector.js')),
  onAttach: function(worker) {
    worker.on('message', function(libraryList) {
      if (!worker.tab.libraries) {
        worker.tab.libraries = [];
      }
      libraryList.forEach(function(library) {
        if (worker.tab.libraries.indexOf(library) == -1) {
          worker.tab.libraries.push(library);
        }
      });
      if (worker.tab == tabs.activeTab) {
        updateWidgetView(worker.tab);
      }
    });
  }
});

The content script is executed once for every window.onload event, so it will run multiple times when a single page containing multiple iframes is loaded. So main.js needs to filter out any duplicates, in case a page contains more than one iframe, and those iframes use the same library.

Implementing the User Interface

Showing the Library Array

The widget module is a natural fit for displaying the library list. We'll specify its content using HTML, so we can display an array of icons. The widget must be able to display different content for different windows, so we'll use the WidgetView object.

main.js will create an array of icons corresponding to the array of library names, and use that to build the widget's HTML content dynamically:

function buildWidgetViewContent(libraryList) {
  widgetContent = htmlContentPreamble;
  libraryList.forEach(function(library) {
      widgetContent += buildIconHtml(icons[library.name],
        library.name + "<br>Version: " + library.version);
  });
  widgetContent += htmlContentPostamble;
  return widgetContent;
}
function updateWidgetView(tab) {
  var widgetView = widget.getView(tab.window);
  if (!tab.libraries) {
    tab.libraries = [];
  }
  widgetView.content = buildWidgetViewContent(tab.libraries);
  widgetView.width = tab.libraries.length * ICON_WIDTH;
}

main.js will use the tabs module to update the widget's content when necessary (for example, when the user switches between tabs):

tabs.on('activate', function(tab) {
  updateWidgetView(tab);
});
tabs.on('ready', function(tab) {
  tab.libraries = [];
});

Showing the Library Detail

The XUL library detector displayed the detailed information about each library on mouseover in a tooltip: we can't do this using a widget, so instead will use a panel. This means we'll need two additional content scripts:

  • one in the widget's context, which listens for icon mouseover events and sends a message to main.js containing the name of the corresponding library:
function setLibraryInfo(element) {
  self.port.emit('setLibraryInfo', element.target.title);
}
var elements = document.getElementsByTagName('img');
for (var i = 0; i < elements.length; i++) {
  elements[i].addEventListener('mouseover', setLibraryInfo, false);
}
  • one in the panel, which updates the panel's content with the library information:
self.on("message", function(libraryInfo) {
  window.document.body.innerHTML = libraryInfo;
});

Finally main.js relays the library information from the widget to the panel:

widget.port.on('setLibraryInfo', function(libraryInfo) {
  widget.panel.postMessage(libraryInfo);
});

Document Tags and Contributors

 Contributors to this page: wbamberg, Sheppy, evold
 Last updated by: wbamberg,