mozilla
Your Search Results

    Creating Reusable Modules

    To follow this tutorial you'll need to have installed the SDK and learned the basics of cfx.

    With the SDK you don't have to keep all your add-on in a single "main.js" file. You can split your code into separate modules with clearly defined interfaces between them. You then import and use these modules from other parts of your add-on using the require() statement, in exactly that same way that you import core SDK modules like page-mod or panel.

    It can often make sense to structure a larger or more complex add-on as a collection of modules. This makes the design of the add-on easier to understand and provides some encapsulation as each module will export only what it chooses to, so you can change the internals of the module without breaking its users.

    Once you've done this, you can package the modules and distribute them independently of your add-on, making them available to other add-on developers and effectively extending the SDK itself.

    In this tutorial we'll do exactly that with a module that calculate file hashes.

    A hashing add-on

    A hash function takes a string of bytes of any length, and produces a short, fixed length string of bytes as output. It's a useful way to create a "fingerprint" that can be used to identify a file. MD5 is a commonly used hash function: although it's no longer considered secure, it works fine outside a security context.

    Here we'll write an add-on that lets the user select a file on disk and calculates its hash. For both these operations we'll use XPCOM interfaces.

    File picker

    To let the user select a file we'll use nsIFilePicker. The documentation for that interface includes an example which we can adapt like this:

    var {Cc, Ci} = require("chrome");
    
    function promptForFile() {
      const nsIFilePicker = Ci.nsIFilePicker;
    
      var fp = Cc["@mozilla.org/filepicker;1"]
               .createInstance(nsIFilePicker);
    
      var window = require("sdk/window/utils").getMostRecentBrowserWindow();
      fp.init(window, "Select a file", nsIFilePicker.modeOpen);
      fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);
    
      var rv = fp.show();
      if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
        var file = fp.file;
        // Get the path as string. Note that you usually won't
        // need to work with the string paths.
        var path = fp.file.path;
        // work with returned nsILocalFile...
      }
      return path;
    }

    Hash function

    Firefox has built-in support for hash functions, exposed via the nsICryptoHash XPCOM interface The documentation page for that interface includes an example of calculating an MD5 hash of a file's contents, given its path. We can adapt it like this:

    var {Cc, Ci} = require("chrome");
    
    // return the two-digit hexadecimal code for a byte
    function toHexString(charCode) {
      return ("0" + charCode.toString(16)).slice(-2);
    }
    
    function md5File(path) {
      var f = Cc["@mozilla.org/file/local;1"]
              .createInstance(Ci.nsILocalFile);
      f.initWithPath(path);
      var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                    .createInstance(Ci.nsIFileInputStream);
      // open for reading
      istream.init(f, 0x01, 0444, 0);
      var ch = Cc["@mozilla.org/security/hash;1"]
               .createInstance(Ci.nsICryptoHash);
      // we want to use the MD5 algorithm
      ch.init(ch.MD5);
      // this tells updateFromStream to read the entire file
      const PR_UINT32_MAX = 0xffffffff;
      ch.updateFromStream(istream, PR_UINT32_MAX);
      // pass false here to get binary data back
      var hash = ch.finish(false);
    
      // convert the binary hash data to a hex string.
      var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
      return s;
    }

    Putting it together

    The complete add-on adds a button to Firefox: when the user clicks the button, we ask them to select a file, compute the hash, and log the hash to the console:

    var {Cc, Ci} = require("chrome");
    
    // return the two-digit hexadecimal code for a byte
    function toHexString(charCode) {
      return ("0" + charCode.toString(16)).slice(-2);
    }
    
    function md5File(path) {
      var f = Cc["@mozilla.org/file/local;1"]
              .createInstance(Ci.nsILocalFile);
      f.initWithPath(path);
      var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                    .createInstance(Ci.nsIFileInputStream);
      // open for reading
      istream.init(f, 0x01, 0444, 0);
      var ch = Cc["@mozilla.org/security/hash;1"]
               .createInstance(Ci.nsICryptoHash);
      // we want to use the MD5 algorithm
      ch.init(ch.MD5);
      // this tells updateFromStream to read the entire file
      const PR_UINT32_MAX = 0xffffffff;
      ch.updateFromStream(istream, PR_UINT32_MAX);
      // pass false here to get binary data back
      var hash = ch.finish(false);
    
      // convert the binary hash data to a hex string.
      var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
      return s;
    }
    
    function promptForFile() {
      var window = require("sdk/window/utils").getMostRecentBrowserWindow();
      const nsIFilePicker = Ci.nsIFilePicker;
    
      var fp = Cc["@mozilla.org/filepicker;1"]
               .createInstance(nsIFilePicker);
      fp.init(window, "Select a file", nsIFilePicker.modeOpen);
      fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);
    
      var rv = fp.show();
      if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
        var file = fp.file;
        // Get the path as string. Note that you usually won't
        // need to work with the string paths.
        var path = fp.file.path;
        // work with returned nsILocalFile...
      }
      return path;
    }
    
    require("sdk/ui/button/action").ActionButton({
      id: "show-panel",
      label: "Show Panel",
      icon: {
        "16": "./icon-16.png"
      },
      onClick: function() {
        console.log(md5File(promptForFile()));
      }
    });
    

    This works , but main.js is now getting longer and its logic is harder to understand. Let's factor the file picker and hashing code into separate modules.

    Creating separate modules

    filepicker.js

    First create a new file in "lib" called "filepicker.js". Copy the file picker code into this new file, and add the following line at the end:

    exports.promptForFile = promptForFile;
    

    This defines the public interface of the new module.

    So "filepicker.js" should look like this:

    var {Cc, Ci} = require("chrome");
    
    function promptForFile() {
      var window = require("sdk/window/utils").getMostRecentBrowserWindow();
      const nsIFilePicker = Ci.nsIFilePicker;
    
      var fp = Cc["@mozilla.org/filepicker;1"]
               .createInstance(nsIFilePicker);
      fp.init(window, "Select a file", nsIFilePicker.modeOpen);
      fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);
    
      var rv = fp.show();
      if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
        var file = fp.file;
        // Get the path as string. Note that you usually won't
        // need to work with the string paths.
        var path = fp.file.path;
        // work with returned nsILocalFile...
      }
      return path;
    }
    
    exports.promptForFile = promptForFile;
    

    md5.js

    Next, create another file in "lib", called "md5.js". Copy the hashing code there, and add this line at the end:

    exports.hashFile = md5File;

    The complete file looks like this:

    var {Cc, Ci} = require("chrome");
    
    // return the two-digit hexadecimal code for a byte
    function toHexString(charCode) {
      return ("0" + charCode.toString(16)).slice(-2);
    }
    
    function md5File(path) {
      var f = Cc["@mozilla.org/file/local;1"]
              .createInstance(Ci.nsILocalFile);
      f.initWithPath(path);
      var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                    .createInstance(Ci.nsIFileInputStream);
      // open for reading
      istream.init(f, 0x01, 0444, 0);
      var ch = Cc["@mozilla.org/security/hash;1"]
               .createInstance(Ci.nsICryptoHash);
      // we want to use the MD5 algorithm
      ch.init(ch.MD5);
      // this tells updateFromStream to read the entire file
      const PR_UINT32_MAX = 0xffffffff;
      ch.updateFromStream(istream, PR_UINT32_MAX);
      // pass false here to get binary data back
      var hash = ch.finish(false);
    
      // convert the binary hash data to a hex string.
      var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
      return s;
    }
    
    exports.hashFile = md5File;

    main.js

    Finally, update main.js to import these two new modules and use them:

    var filepicker = require("./filepicker.js");
    var md5 = require("./md5.js");
    
    require("sdk/ui/button/action").ActionButton({
      id: "show-panel",
      label: "Show Panel",
      icon: {
        "16": "./icon-16.png"
      },
      onClick: function() {
        console.log(md5.hashFile(filepicker.promptForFile()));
      }
    });

    You can distribute these modules to other developers, too. They can copy them somewhere under the add-on, and include them using require() in the same way.

    Learning More

    To see some of the modules people have already developed, see the page of community-developed modules. To learn how to use third-party modules in your own code, see the tutorial on adding menu items.

    Document Tags and Contributors

    Tags: 
    Contributors to this page: wbamberg, Canuckistani, pc.wiz.tt, tomica, maybe
    Last updated by: pc.wiz.tt,
    Hide Sidebar