Translate

Archives

Using JavaScript Code Modules in Firefox 4 Add-Ons

The concept of a JavaScript code module in the Gecko layout engine was first introduced in Gecko 1.9. This post discusses how such code modules can be used to simplify preference and add-on management in Firefox 4 which uses Gecko 2.0 and JavaScript 1.8.5. It uses a simple Firefox add-on called HTML5toggle as an example of how to modify existing code to use Javascript code modules.

A JavaScript code module is simply some JavaScript code located in a registered (well-known) location. JavaScript code modules are primarily used to share code between different privileged scopes. They can also be used to create global JavaScript singletons that previously required using JavaScript XPCOM objects.

Here is a list of the current JavaScript code modules in Firefox 4.0:

NamePurpose
AddonManager.jsmProvides routines to install, manage, and uninstall add-ons.
AddonRepository.jsmAllows searching of the add-ons repository.
ctypes.jsmInterface that allows JavaScript code to call native libraries without requiring the development of an XPCOM component.
DownloadLastDir.jsmProvides the path to the directory into which the last download occurred.
Geometry.jsmProvides routines for performing basic geometric operations on points and rectangles.
ISO8601DateUtils.jsmProvides routines to convert between JavaScript Date objects and ISO 8601 date strings.
NetUtil.jsmProvides networking utility routines, including the ability to easily copy data from an input stream to an output stream asynchronously.
openLocationLastURL.jsmProvides access to the last URL opened using the "Open Location" option in the File menu.
PerfMeasurement.jsmProvides access to low-level hardware and OS performance measurement tools.
PluralForm.jsmProvides an easy way to get the correct plural forms for the current locale, as well as ways to localize to a specific plural rule.
PopupNotifications.jsmProvides an easy way to present non-modal notifications to users.
Services.jsmProvides getters for conveniently obtaining access to commonly-used services.
XPCOMUtils.jsmContains utilities for JavaScript components loaded by the JavaScript component loader.

The Components.utils.import method is used to import a JavaScript code module into a specific JavaScript scope. A module must be imported before any functionality in the module is used. Modules are cached when loaded and subsequent imports do not reload a new version of the module, but instead use the previously cached version. Thus a given module is shared when imported multiple times.

One of the most commonly used modules is the Services.jsm module which offers a wide assortment of lazy getters that simplify the process of obtaining references to commonly used services. A lazy getter is a getter function that checks the item which it is to retrieve (get) and initializes it if it is not set.

Here is a list of the available services in Services.jsm:

Service AccessorService InterfaceService Name
appinfonsIXULAppInfo nsIXULRuntimeApplication information service
consolensIConsoleServiceError console service
dirsvcnsIDirectoryService nsIPropertiesDirectory service
droppedLinkHandlernsIDroppedLinkHandlerDropped link handler service
ionsIIOService nsIIOService2I/O Service
localensILocaleServiceLocale service
obsnsIObserverServiceObserver service
permsnsIPermissionManagerPermission manager service
prefsnsIPrefBranch nsIPrefBranch2
nsIPrefService
Preferences service
promptnsIPromptServicePrompt service
scriptloadermozIJSSubScriptLoaderJavaScript subscript loader service
searchnsIBrowserSearchServiceBrowser search service
storagemozIStorageServiceStorage API service
stringsnsIStringBundleServiceString bundle service
tmnsIThreadManagerThread Manager service
vcnsIVersionComparatorVersion comparator service
wmnsIWindowMediatorWindow mediator service
wwnsIWindowWatcherWindow watcher service

For Firefox 4 the add-on manager was extensively reworked. It is now based on a JavaScript code module called AddonManager.jsm. See the source code for AddonManagerInternal in …/mozapps/extensions/AddonManager.jsm for full details. Visually it is implemented as a tab instead of a separate window and uses icons that are 64×64 pixels instead of 32×32 pixels. Add-on metadata is now stored in an SQLite database instead of RDF-based storage.

Here is a list of the available methods in AddonManager.jsm:

METHODPURPOSE
getInstallForURLAsynchronously gets an AddonInstall for a URL
getInstallForFileAsynchronously gets an AddonInstall for an nsIFile
getAddonByIDAsynchronously gets an add-on with a specific ID
getAddonsByIDsAsynchronously gets an array of add-ons
getAddonsWithOperationsByTypesAsynchronously gets add-ons that have operations waiting for an application restart to complete
getAddonsByTypesAsynchronously gets add-ons of specific types
getAllAddons Asynchronously gets all installed add-ons
getInstallsByTypesAsynchronously gets all current AddonInstalls optionally limiting to a list of types
getAllInstallsAsynchronously gets all current AddonInstalls
isInstallEnabledCheck whether installation is enabled for a particular mimetype
isInstallAllowedChecks whether a particular source is allowed to install add-ons of a given mimetype
installAddonsFromWebpageStarts installation of an array of AddonInstalls notifying the registered web install listener of blocked or started installs
addInstallListenerAdds a new InstallListener if the listener is not already registered
removeInstallListenerRemoves an InstallListener if the listener is registered
addAddonListenerAdds a new AddonListener if the listener is not already registered
removeAddonListenerRemoves an AddonListener if the listener is registered

Note that all of the above methods are asynchronous which mean that a callback function is required. The callback may well only be called after the method returns.

In earlier versions of Firefox, you have to observe em-action-requested to determine whether an add-on should be uninstalled or not. For example, the following code could be used in Firefox 3 to detect when to cleanup up the preferences for HTML5toggle during add-on removal.

  var uninstall = NULL,

  observe: function(subject, topic, data)
   {
       if (topic == "em-action-requested") {
          subject.QueryInterface(Components.interfaces.nsIUpdateItem);
          if (subject.id == MY_EXTENSION_UUID) {
             if (data == "item-uninstalled") {
                 this.uninstall = true;
             } else if (data == "item-cancel-action") {
                 this.uninstall = false;
             }
          }
       }

       if (topic == "quit-application-granted") {
           if (this.uninstall) { 
               var prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                           .getService(Components.interfaces.nsIPrefService);
               var prefBranch = prefService.getBranch('extensions.html5toggle.');
               prefBranch.deleteBranch("");             
           }
           this.unregister();
       }      
   },

   register: function()
   {  
       var observerService = Components.classes["@mozilla.org/observer-service;1"].
                             getService(Components.interfaces.nsIObserverService);
     
       observerService.addObserver(this, "em-action-requested", false);
       observerService.addObserver(this, "quit-application-requested", false);
       observerService.addObserver(this, "quit-application-granted", false);
   },


   unregister : function() 
   {
       var observerService = Components.classes["@mozilla.org/observer-service;1"].
                             getService(Components.interfaces.nsIObserverService);

       observerService.removeObserver(this, "em-action-requested");
       observerService.removeObserver(this, "quit-application-granted");
       observerService.removeObserver(this, "quit-application-requested");
   }


This code no longer works in Firefox 4. Instead, an add-on needs to handle two events, onUninstalling and onOperationCancelled, as shown in below.

Here is the source code for overlay.js prior to being upgraded to using JavaScript code modules. It is from HTML5toggle v1.02.

const MY_EXTENSION_UUID = "{16f32596-71ca-4053-a4f3-cfc8b157f541}";

var html5toggle = {

   beingUninstalled: null,

   onLoad: function() {
      var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                 .getService(Components.interfaces.nsIPrefService).getBranch("html5.");
      var value = prefs.getBoolPref("enable");

      // save the current value of html5.enable preference in our own branch
      var myprefs = Components.classes["@mozilla.org/preferences-service;1"]
                    .getService(Components.interfaces.nsIPrefService);
      var mybranch = myprefs.getBranch('extensions.html5toggle.');
      mybranch.setBoolPref('enable', value);
      myprefs.savePrefFile(null);

      this.register();
      this.setTBbutton(value);
      this.setMIchecked(value);
   },

   setMIchecked: function (value) {
      var menuitem = document.getElementById('html5toggle-menuitem');
      if (value) {
          menuitem.setAttribute("checked", "false"); 
      } else {
          menuitem.setAttribute("checked", "true"); 
      }
   },

   setTBbutton: function (value) {
      var tbButton = document.getElementById('html5toggle-toolbar-button');
      var stringsBundle = document.getElementById('html5toggle-string-bundle');

      if (tbButton) {            // button might still be on Toolbar Palette
         if (value) {
            tbButton.tooltipText = stringsBundle.getString('onString');
            tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5on16N.png");
         } else {
            tbButton.tooltipText = stringsBundle.getString('offString');
            tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5off16N.png");
         }
      }
   },

   onToolbarButton: function(e) {
      var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                  .getService(Components.interfaces.nsIPrefService).getBranch("html5.");
      var value = prefs.getBoolPref("enable");
 
      value = !value;
      prefs.setBoolPref("enable", value); 

      this.setTBbutton(value);
      this.setMIchecked(value);
   },

   onMenuItem: function(e) {
      var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                  .getService(Components.interfaces.nsIPrefService).getBranch("html5.");
      var value = prefs.getBoolPref("enable");

      var menuitem = document.getElementById('html5toggle-menuitem');
      value = menuitem.getAttribute("checked") == "true" ? false : true;

      prefs.setBoolPref("enable", value); 
      this.setTBbutton(value);
   },

   observe: function(subject, topic, data)
   {
       if (topic == "quit-application-granted") {

           dump("\nhtml5toggle:quit-application-granted ....");

           if (this.beingUninstalled) {
               // restore initial value of html5.enable preference
               var myprefs = Components.classes["@mozilla.org/preferences-service;1"]
                             .getService(Components.interfaces.nsIPrefService);
               var mybranch = myprefs.getBranch('extensions.html5toggle.');
               var value = mybranch.getBoolPref("enable");
               mybranch.deleteBranch("");             

               var prefs = Components.classes["@mozilla.org/preferences-service;1"]
                             .getService(Components.interfaces.nsIPrefService);
               var branch = prefs.getBranch('html5.');
               branch.setBoolPref('enable', value);
               prefs.savePrefFile(null);
           }
           this.unregister();
       }       
   },

   onUninstalling: function(addon) {
       if (addon.id == MY_EXTENSION_UUID) {
           this.beingUninstalled = true;
       }
   },

   onOperationCancelled: function(addon) {
       if (addon.id == MY_EXTENSION_UUID) {
           this.beingUninstalled = (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) != 0;
       }
   },  

   register: function()
   {  
       dump("\nhtml5toggle:register ....");
        
       var observerService = Components.classes["@mozilla.org/observer-service;1"].
                             getService(Components.interfaces.nsIObserverService);
     
       observerService.addObserver(this, "quit-application-granted", false);

	  Components.utils.import("resource://gre/modules/AddonManager.jsm");
	  AddonManager.addAddonListener(this);
   },

   unregister: function() 
   {
       dump("\nhtml5toggle:unregister ....");

       var observerService = Components.classes["@mozilla.org/observer-service;1"].
                             getService(Components.interfaces.nsIObserverService);
    
       observerService.removeObserver(this, "quit-application-granted");
       AddonManager.removeAddonListener(this);
   }
};

window.addEventListener("load", function () { html5toggle.onLoad(); }, false);


Here is the same source file after being modified to use JavaScript code modules. It is from HTML5toggle v1.03.

const MY_EXTENSION_UUID = "{16f32596-71ca-4053-a4f3-cfc8b157f541}";
const PREF_BRANCH_HTML5 = Services.prefs.getBranch("html5.");
const PREF_BRANCH_HTML5TOGGLE = Services.prefs.getBranch("extensions.html5toggle.");
const OBSERVER = Services.obs;
const ADDONMANAGER = AddonManager;


var html5toggle = {

   beingUninstalled: null,

   onLoad: function() {
      let value = PREF_BRANCH_HTML5.getBoolPref("enable");

      // save the current value of html5.enable preference in our own branch
      PREF_BRANCH_HTML5TOGGLE.setBoolPref('enable', value);
      Services.prefs.savePrefFile(null);

      this.register();
      this.setTBbutton(value);
      this.setMIchecked(value);
   },

   setMIchecked: function (value) {
      let menuitem = document.getElementById('html5toggle-menuitem');
      if (value) {
          menuitem.setAttribute("checked", "false"); 
      } else {
          menuitem.setAttribute("checked", "true"); 
      }
   },

   setTBbutton: function (value) {
      let tbButton = document.getElementById('html5toggle-toolbar-button');
      let stringsBundle = document.getElementById('html5toggle-string-bundle');

      if (tbButton) {            // button might still be on Toolbar Palette
         if (value) {
            tbButton.tooltipText = stringsBundle.getString('onString');
            tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5on16N.png");
         } else {
            tbButton.tooltipText = stringsBundle.getString('offString');
            tbButton.setAttribute("image","chrome://html5toggle/skin/images/HTML5off16N.png");
         }
      }
   },

   onToolbarButton: function(e) { 
      let value = PREF_BRANCH_HTML5.getBoolPref("enable");
      PREF_BRANCH_HTML5.setBoolPref("enable", !value); 

      this.setTBbutton(value);
      this.setMIchecked(value);
   },

   onMenuItem: function(e) {
      let menuitem = document.getElementById('html5toggle-menuitem');
      let value = menuitem.getAttribute("checked") == "true" ? false : true;

      PREF_BRANCH_HTML5.setBoolPref("enable", value); 
      this.setTBbutton(value);
   },

   observe: function(subject, topic, data)
   {
       if (topic == "quit-application-granted") {

           dump("\nhtml5toggle:quit-application-granted ....");

           if (this.beingUninstalled) {
               // restore initial value of html5.enable preference
               let value = PREF_BRANCH_HTML5TOGGLE.getBoolPref("enable");
               PREF_BRANCH_HTML5TOGGLE.deleteBranch("");             
               PREF_BRANCH_HTML5.setBoolPref('enable', value);
               Service.prefs.savePrefFile(null);
           }
           this.unregister();
       }       
   },

   onUninstalling: function(addon) {
       if (addon.id == MY_EXTENSION_UUID) {
           this.beingUninstalled = true;
       }
   },

   onOperationCancelled: function(addon) {
       if (addon.id == MY_EXTENSION_UUID) {
           this.beingUninstalled = (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) != 0;
       }
   },  

   register: function()
   {  
       dump("\nhtml5toggle:register ....");
        
       OBSERVER.addObserver(this, "quit-application-granted", false);
	  ADDONMANAGER.addAddonListener(this);
   },

   unregister: function() 
   {
       dump("\nhtml5toggle:unregister ....");

       OBSERVER.removeObserver(this, "quit-application-granted");
          ADDONMANAGER.removeAddonListener(this);
   }
};

Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/AddonManager.jsm");
window.addEventListener("load", function () { html5toggle.onLoad(); }, false);


As you can see if you study the above source code, using JavaScript code modules reduces the number of lines of code and makes the source code more readable. I assume you are familiar with JavaScript in particular and Mozilla add-ons in general if you are reading this post so I am not going to try and explain the above code in any detail.

You should also have noticed that I updated the source code to use the JavaScript let keyword instead of var where possible. This keyword was introduced in JavaScript 1.7 which is a Mozilla only extension. Variables declared by let have local scope, i.e. their scope (visibility) is the block in which they are defined as well as in any sub-blocks in which they are not redefined. Contrast that with variables declared by var which have as their scope the entire enclosing function.

Consider the following short JavaScript example:

var i = 0;
for ( let i = i; i < 2 ; i++ )
   print ( "> " + i );
print ( i );


If you used the var keyword in the for statement, the variable would be visible within the whole function containing the loop. To reduce the visibility of the variable to just the scope of the for loop, we use the let keyword. Using the Tracemonkey JavaScript engine (which supports up to Javascript v1.85) shell, the output from this code snippet is:

> 0
> 1
> 2
0


There is an enormous amount of useful functionality in the current set of JavaScript code modules. You need to read the appropriate documentation for each code module to gain an understanding of what is available in each module. Unfortunately the documentation is written in the usual terse style adopted by the Mozilla developers and use case examples are scarce.

For example, if you examine the Services.jsm documentation page, you will notice a prompt service accessor. This provides access to a number of useful methods including alert which shows an alert dialog with an OK button. It works the same way as window.alert but accepts a title for the dialog.

Components.utils.import("resource://gre/modules/Services.jsm");
....
Services.prompt.alert(null, "My blog", "Hello there, dear reader of my blog");


The current set of JavaScript code modules does not include string localization. I would like to see somebody develop a good JavaScript code module for string localization as that would simplify the coding of restartless add-ons.

Well, that is all that your should need to know to get you started on updating your Firefox add-ons to take advantage of JavaScript code modules. One thing to watch out for with JavaScript code modules is that you do not break backwards compatibility if your add-on is expected to work with both Firefox 3 and Firefox 4.Good luck!

P.S. The full source code for HTML5toggle versions 1.02 and 1.03 is available on my website.

Comments are closed.