Local Storage

Note: You may rather be looking for DOM Storage (localStorage, etc.), unless you really are doing XUL development.

It is very common for an extension to require some kind of local persistent storage. We recommend that you at least keep an error log, so that you can request error data from your users when you encounter problems that are hard to debug. We'll discuss logging in this section, but first let's look at the right (or at least, common and scalable) way of managing local files.

It is strongly recommended that you keep your local files inside the Firefox profile directory. Otherwise you may run into problems if the same extension is installed in multiple profiles. The common practice is to create a directory with the name of your project at the root of the profile folder, and keep your files inside. The structure could be something like this:

  • s435L.default (your profile directory)
    • XULSchool
      • log.txt
      • somedbfile.sqlite

The Directory Service and the nsIFile interface are used to create the local directory. Here's what we usually do: we have a function that returns a reference to our root directory and creates it if necessary.

getLocalDirectory : function() {
  let directoryService =
    Cc["@mozilla.org/file/directory_service;1"].
      getService(Ci.nsIProperties);
  // this is a reference to the profile dir (ProfD) now.
  let localDir = directoryService.get("ProfD", Ci.nsIFile);

  localDir.append("XULSchool");

  if (!localDir.exists() || !localDir.isDirectory()) {
    // read and write permissions to owner and group, read-only for others.
    localDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0774);
  }

  return localDir;
},

ProfD is a special identifier for the profile directory that exists so that you don't need to figure out its location. In general this is the only directory flag you'll need, but sometimes you'll need access to other system directories, and you don't want to have to worry about which operating system or system language your extension is running on. A full list of these flags can be found in the Firefox source.
Having that function in place, we do something like the following:

let myFile = XULSchool.getLocalDirectory();

myFile.append("someFile.txt");
// do stuff with the file.

Files are handled with the nsIFile interface. An nsIFile doesn't necessarily represent an existing file, as in the previous examples. You first specify the file using nsIFile and then use nsIFile.create() to actually write it out to disk. You can also check if an nsIFile exists using nsIFile.exists().

To read and write information in files, you need to use stream objects. You can read more about reading and writing files here. In general you won't need to do this directly, but it's always useful to know.

Finally, there's the issue of deleting local files when the extension is going to be uninstalled. Whether this is necessary or not is a matter of preference. Some extension developers prefer to leave the data there, so that if the user chooses to install the extension again, all the previous data will be recovered. This is the default behavior when Firefox is uninstalled: the profile information remains intact and it will be there waiting for you if Firefox is installed again. Others feel concerned about privacy and storing private information locally without deleting it. A good argument can be done for both, so it is up to you to choose what to do in this case. The FUEL library has an uninstall event you can use to perform these operations.

Logging

Good logging is essential in all kinds of software projects. Any extension that is more complex than a Hello World needs some way to log errors and trace execution without having to fire up a debugger.

It used to be the case that custom logging solutions were necessary, but Mozilla Labs have come up with a JavaScript implementation of a logger similar to the Log4J logger used in Java projects. The logger is called Log4Moz and it is implemented as a JavaScript Code Module, so it only works on Firefox 3 and above.

To use this logger, you need to copy the log4moz.js file to your modules directory. In the initialization method of your one of your "common" or startup objects, add the following code:

let formatter = new Log4Moz.BasicFormatter();
let root = Log4Moz.repository.rootLogger;
let logFile = this.getLocalDirectory(); // remember this?
let appender;

logFile.append("log.txt");

// Loggers are hierarchical, lowering this log level will affect all
// output.
root.level = Log4Moz.Level["All"];

// this appender will log to the file system.
appender = new Log4Moz.RotatingFileAppender(logFile, formatter);
appender.level = Log4Moz.Level["All"];
root.addAppender(appender);

After that, you can create a logger object for any object in your project like this:

this._logger = Log4Moz.repository.getLogger("XULSchool.SomeObject");

this._logger.level = Log4Moz.Level["All"];

Note: We recommend that you create a logger instance in the constructor of every object and store it in a private variable.

And then logging is done with any of the following methods, depending on the kind of message being logged:

this._logger.fatal("This is a fatal message.");
this._logger.error("This is an error message.");
this._logger.warn("This is a warning message.");
this._logger.info("This is an info message.");
this._logger.config("This is a config message.");
this._logger.debug("This is a debug message.");
this._logger.trace("This is a trace message.");

You can filter the output of the global logger or any specific logger instance by setting the level property. During development you should use the "All" level, but for release versions it's usually better to move the level up to "Warn", so that the log is compact and execution is more efficient.

Note: We recommend that all exception catch blocks include some logging at the error or warn levels, and in general you should use logging freely in order to have as much information as possible to fix bugs and know what is going on. Don't log inside functions that are called too often, such as mouseover event handlers, or certain HTTP activity listeners. This impacts performance and fills the log with useless messages. We normally add a comment that indicates that logging is not done there for performance reasons.

SQLite

SQLite storage was introduced in Firefox 2, and it's the preferred storage mechanism in Firefox. It is the storage system used for the Places API that manages bookmarks and history. It's also used for storing cookies, form inputs, and others.

SQLite is a lightweight SQL based storage system. It is ideal for embedding in other programs, and is currently in use in several popular applications. It's also the storage system we recommend for local storage in extensions.

The Storage page has a good explanation on how to use the SQLite API, so we won't go over that again. If you're unfamiliar with SQL or if you're interested in knowing the restrictions in the syntax used by SQLite, you can read more at the SQLite site.

You should carefully design your database structure, taking into account features you're planning on adding in the future. Adding or removing columns, or making other changes to your DB structure from one version of your extension to the next will probably cause breakage of user data in older versions. You'll need to carefully add migration code that moves the data from the old DB format to the new, and this becomes increasingly complex as you add new versions and new structure changes. So, be careful and plan for the future.

There are two paths you can take when creating the local database you'll be using for your extension:

  • Generate the database file (through mozIStorageService.openDatabase(), all tables (through mozIStorageConnection.createTable(), and initial data when your extension starts up for the first time. If you need a complex database this can be heavy in terms of time and code, but this will only happen once and this can be done in a lazy or asynchronous way.
  • Have an initial database file in your defaults directory that you can copy to the user's profile. This way you just need to do a file copy. You can reach the defaults directory at ProfD/extensions/YOUR_EXTENSION_ID.xpi/defaults for packed extensions or ProfD/extensions/YOUR_EXTENSION_ID/defaults for unpacked extensions. Packed extensions have appear with Gecko 2.0 (Firefox 4.0). This approach has the downside of being less stable.

RDF

RDF used to be the preferred storage mechanism for Firefox. If was used for most of its datasources, and you can still see it in use here and there, like in the install.rdf file. It is being phased out, with SQLite taking its place in most cases. The RDF API may be removed at some point in the future because it requires a great deal of code even for the simplest tasks, and it currently sees little maintenance, so we don't recommend using it unless you really have to.

You'll still need to understand at least a little about RDF when you read the documentation about templates.

Templates

Templates are a very powerful tool in Firefox. They allow you to automatically generate XUL content using information from a datasource, and automatically update the content once the datasource changes. They were designed with RDF datasources in mind, but since Firefox 3 they have been extended to support SQLite datasources as well.

Handling templates can also be complicated, but it is worth the effort if you need to display long lists or trees with complex data. If you manage to get your display code to use templates, you will have saved a lot of coding. Since templates are not necessary for most extensions and they're a complicated subject, it's better that you read it from the experts. There's a very detailed XUL Template Guide here. As mentioned before, it revolves around RDF, so you may need to take some time to understand how RDF works. There's a section about SQLite Templates in the guide, but there are some concepts in it that will require you to read at least some of the rest of it.

This tutorial was kindly donated to Mozilla by Appcoast.

Document Tags and Contributors

Contributors to this page: Jorge.villalobos, DaveG, trevorh, wbamberg, kscarfone, teoli, madarche
Last updated by: wbamberg,