Chapter 5: Let's build a Firefox extension

In This Article
  1. Setting up your development environment
    1. Create a development profile
    2. Change your preferences for more efficient development
    3. Install the DOM Inspector
    4. Optional - Install Firebug and Chromebug
  2. Developing extensions: What you need to know
    1. Chrome
      1. What is chrome?
      2. Three kinds of packages make up chrome
      3. Chrome URL
    2. Cross-package overlays
    3. Conclusion
  3. Developing a Simple Extension: Hello World
    1. Phase 1: test install
      1. Source file structure
      2. Create install manifest
      3. Create the chrome manifest
      4. Test install and operational check
    2. Phase 2: Adding a function to display the time
      1. Adding an event handler
      2. Clock handler
      3. Operations check
    3. Phase 3: Adding multilingual support
      1. Directory structure
      2. Adding locale packages
      3. Replacing text with entity references in XUL
      4. Create the DTD that defines the entity references
      5. Replacing User Interface messages within JavaScript files with properties references
      6. Operations check
    4. Phase 4: Adding a toolbar button
      1. Directory structure
      2. Add the skin package
      3. Add the toolbar button
      4. Create the style sheet
      5. Operations check
    5. Phase 5: XPI packaging
      1. XPI file directory structure
      2. Packaging procedure
      3. XPI operations check
  4. Developing practical extensions: A session-management extension
    1. Phase 1: test install
      1. Source file structure
      2. Creating install manifest
      3. Creating chrome manifest
      4. Finding overlay merge points
      5. Browser window overlay
      6. Test install
    2. Phase 2: Implement functionality
      1. Figure out JavaScript skeleton
      2. Adding event handlers
      3. Implementing methods
    3. Phase 3: Creating a preference panel
      1. Directory structure
      2. Deciding the format for your preferences
      3. Defaults definition file
      4. Creating the preference panel
      5. Changing behavior through preference values
      6. Operations check
    4. Phase 4: XPI packaging
  5. Conclusion
    1. Operations checks on revised source files
      1. XUL that opens new windows
      2. XUL overlaying other windows
      3. properties file
      4. XPCOM components inside the components directory
      5. Default preferences file inside the defaults\preferences directory
      6. chrome.manifest
      7. install.rdf
    2. The session store API
      1. getBrowserState()
      2. setWindowState(aWindow, aState, aOverwrite)
    3. JavaScript debugging methods
      1. alert
      2. dump
      3. error console
    4. Where can I learn more about the XPCOM interface?

Obsolete
This feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time. Try to avoid using it.

Draft
This page is not complete.

This document was authored by Taiga (Gomita) Gomibuchi and was originally published in Japanese for the Firefox Developers Conference Summer 2007. Gomita-san won "Most Useful Upgraded Extension" award in Mozilla's 2006 "Extend Firefox" competition for ScrapBook, and was runner-up in the "Extend Firefox 2" contest for FireGestures.

The chapters so far have each focused on technologies in isolation—XUL, JavaScript, CSS, and XPCOM. In this chapter, we’ll discuss how to put them together to actually build an extension.

An easier method of building a Firefox/Thunderbird Addon for developers who are well-acquainted with IDEs like Netbeans, Eclipse, etc. is to use Netbeans (IDE from Sun Microsystems) and Foxbeans (Plugin for Netbeans, by TeeSoft).

Setting up your development environment

In order to develop (and test) extensions in a most convenient way it is recommended to apply some changes to Firefox. In the following you will find a brief description how to do so; a more detailed one can be found under Setting up an extension development environment.

Create a development profile

If you want to partition your everyday browsing environment from your development environment in Firefox, set up a second profile for development. To create this dev profile start Firefox with

firefox.exe -no-remote -P dev

On the first start the Profile Manager will appear, where you can create the dev profile and configure its home-path.

Change your preferences for more efficient development

Before you get to work developing your extension, you’ll want to change some of your Firefox preferences. This isn’t a requirement for extension development, but I recommend it as a way to work more efficiently.

Preference Description Value

nglayout.debug.disable_xul_cache (not present in Firefox 3.5+)

Ordinarily, Firefox will cache XUL documents after they have been read in once, to speed subsequent displays. Disabling this cache forces XUL documents to be reloaded any every time they are displayed. True

browser.dom.window.dump.enabled (not present in Firefox 3.5+)

Enables use of the dump method for debugging. Refer to the “JavaScript debugging methods” sidebar. True

javascript.options.showInConsole (present in Firefox 3.5+)

Outputs JavaScript errors to the error console. True
javascript.options.strict (present in Firefox 3.5+) Enforces strict error output from JavaScript True

Table 1: Preferences to set for developing extensions

To make these changes, start your development profile, type about:config into Firefox’s location bar and open the preferences window; find the preferences listed in Table 1 and double-click on them to set them accordingly. Some of these preferences do not exist—to create them, right-click, select “New>Boolean”, and type in the name and set the value accordingly.

Install the DOM Inspector

The DOM Inspector is an extension that lets you examine HTML and XUL DOM tree structures, JavaScript objects and CSS properties, etc. This is not required for developing extensions, but it is handy to have around. Install extensions for Firefox from the Mozilla Add-ons website

Optional - Install Firebug and Chromebug

Chromebug is used by the Firebug development team to develop Firebug.  You can use it to help debug Firefox extensions as well as to inspect and learn how the Firefox UI (Chrome) is constructed. You can learn more about Chromebug and download it at   http://getfirebug.com/wiki/index.php/Chromebug_User_Guide

You may also find this extension to be valuable: Extension Developer https://addons.mozilla.org/en-US/firefox/addon/7434  

Developing extensions: What you need to know

Let’s delve into chrome, something you’ll need to know about in order to develop extensions.

Chrome

What is chrome?

Chrome”is the word used to describe all the GUI structural elements that go into an XUL application. For example, in the Firefox browser window, everything other than the web page being displayed in the content area is chrome. Extensions also can be considered XUL applications with chrome.

Three kinds of packages make up chrome

The content package

This package is used to contain the main XUL and JavaScript source files. Most extensions consist of a single content package2

The locale package

This package is used to contain language data that can be translated. To make an extension’s GUI support multiple languages, you can include multiple locale packages, one for each language.

The skin package

This is used to include source files used as visual elements in the GUI, including style sheets and images. Most extensions include only one skin package, but you can include multiple skin packages to allow the GUI to change with different themes.

Chrome URL

Use a file called a “chrome manifest” to register chrome packages with Firefox and start using them. To register a package, you use a special URI scheme called a “Chrome URL” to represent the path to the file. Chrome URLs are structured as:

chrome://%package name%/%package type%/%relative path to source file%

For example, the Chrome URL for the Firefox browser window is as follows:

chrome://browser/content/browser.xul

Here, the package name is “browser”, the file “browser.xul”, and the type of the package is “content”. Source files represented through chrome URLs run with UniversalXPConnect privileges; even if they haven’t been granted “use XPConnect as local file” privileges, as discussed in Chapter 4, they will be able to use XPCOM functions (Figure 1).

Extension/
  chrome manifest
  Chrome/ #registered in the chrome
    content package #Run with privileges
    locale package
    skin package

Figure 1: Chrome packages and chrome registration (FIXME: Not really explicit and not a Figure)

Cross-package overlays

The “overlay” technique introduced in Chapter 3 required the use of the xul-overlay instruction in the XUL document that is the overlay target. It is also possible to coerce an overlay without using the xul-overlay instruction in the XUL document—this is called a “cross-package overlay” (Figure 2). In fact, you need to use cross-package overlays to append buttons or menus to the Firefox browser window. Use the chrome manifest to invoke a cross-package overlay.

Normal overlay: Adding the xul-overlay instruction to the target XUL document overlays it with another XUL document
Cross-package overlay: Adding a cross manifest permits one XUL file to overlay another without any xul-overlay instruction.

Figure 2: Invoking a cross-package overlay (FIXME: Not really explicit and not a Figure)

Conclusion

This has been a brief introduction to chrome that probably leaves a lot of unanswered questions. If you keep in mind the following three points at the very least, the next section, Developing a Simple Extension, should help flesh out your understanding.

  1. An extension’s GUI, or chrome, can consist of three kinds of package: content, locale, and skin.
  2. Use a cross-package overlay on top of browser.xul to add a button or menu item to the Firefox browser window.
  3. The chrome manifest plays two important roles: it registers the contents of the chrome packages, and invokes the cross-package overlay.

Developing a Simple Extension: Hello World

In this section, we will write an extremely simple “hello world” extension that only displays the time.3

 

Phase 1: test install

Our first step will be to perform a test installation consisting of the minimum code needed to add a new menu item to the Tools menu (Figure 3).

FIXME: Figure 3: Menu after Phase 1 complete

Source file structure

First set up the work folders you’ll use to organize your extension’s source files. The folder can be wherever you want, but for the purposes of this guide, we’ll assume it’s at C:\extensions\helloworld . Create folders and files in your work folder as shown in Figure 4. The purpose of each file created during Phase 1 is explained in Table 2.

FIXME: Figure 4: Folder structure used in Phase 1

Table 2: How files are used in Phase 1

FIXME: Make the table cleaner

File name

Role

install.rdf

Called the install manifest, this gives basic information about the extension, and is required in order for the extension to be installed in Firefox.

chrome.manifest

This is the chrome manifest described in the earlier section. Registers packages and invokes cross-package overlays.

content/overlay.xul

XUL file that will be overlaid on the Firefox browser window, adding buttons, menu items, etc.

content/clock.xul

content/clock.js

The XUL to display a clock in the window, and the JavaScript to control its operation (these files will be used in Phase 2).

Create install manifest

Fill in the install.rdf file as shown in Listing 1.

Listing 1: Content for install.rdf

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
    <!-- Unique ID for extension. Can be in e-mail address format or GUID format -->
    <em:id>helloworld@xuldev.org</em:id>
    <!-- Indicates that this add-on is an extension -->
    <em:type>2</em:type>
    <!-- Extension name displayed in Add-on Manager -->
    <em:name>Hello, World!</em:name>
    <!-- Extension version number. There is a version numbering scheme you must follow -->
    <em:version>0.1</em:version>
    <!-- Brief description displayed in Add-on Manager -->
    <em:description>My first extension.</em:description>
    <!-- Name of extension's primary developer. Change to your name -->
    <em:creator>Gomita</em:creator>
    <!-- Web page address through which extension is distributed -->
    <em:homepageURL>http://www.xuldev.org/helloworld/</em:homepageURL>
    <!-- This section gives details of the target application for the extension (in this case: Firefox 2) -->
    <em:targetApplication>
      <Description>
        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
        <em:minVersion>2.0</em:minVersion>
        <em:maxVersion>4.0.0.*</em:maxVersion>
      </Description>
    </em:targetApplication>
  </Description>
</RDF>

Create the chrome manifest

Fill in the chrome.manifest file as shown in Listing 2.

Listing 2: Content for chrome.manifest

content helloworld content/
overlay chrome://browser/content/browser.xul chrome://helloworld/content/overlay.xul
Register the content package (content instruction)

Line 1, beginning content, contains the code used to register the chrome’s content package. Here, helloworld is a package name, and content/ is a relative path to the folder where the source file is stored. Registering the content package makes it so that the overlay.xul file in the content folder can be accessed using the chrome URL, chrome://helloworld/content/overlay.xul.

XUL cross-package overlay (overlay instruction)

Line 2, beginning overlay, contains the code used to invoke the cross-package overlay, overlaying the Firefox browser window, at chrome://browser/content/browser.xul with chrome://helloworld/content/overlay.xul.

Finding overlay merge points

The next step is to find the “merge points” where the overlay document will insert its content into the base document. There are a number of ways to go about this; here, we’ll use the DOM Inspector (Figure 5).

  1. Open the DOM Inspector by selecting the Tools > DOM Inspector menu item.
  2. Type chrome://browser/content/browser.xul into the URL input field at the top, then click the Inspect button. A browser pane will appear in the lower part of the window.
  3. Click the spots numbered 1 and 2 in Figure 5, in that order; the menu element (3) will then be selected.
  4. Expand spot 3; this will disclose the area numbered 4, with several menuitem elements. The menupop element 4 contains the merge point where you’re going to be adding a new menu item; you can see that it has the id menu_ToolsPopup.
  5. Look through the menuitem elements within this menupop element and determine where you want to add your new element. For the time being, let’s locate it in the spot numbered 5.

FIXME: Figure 5: Finding merge points for overlays using the DOM Inspector

Putting overlays on the browser window

Now that you know the merge point for your overlay, you can create the overlay.xul file that will insert it (Listing 3). In line 4, we identify the merge point as the menupop element; in lines 5–7, we have the code that adds the new menu item. The insertbefore attribute determines where the element gets added.

Test install and operational check

This brings us to the point where we finally install our Hello World extension. Using the normal XPI-style installer would just be added trouble in this case, so we won’t use it. For source files under development, we do test installs.

Placing pointer files

First, navigate to the profile folder of the currently active profile4, and open the extensions folder within it. Create a new file there with the name you used as the Hello World extension ID in the install manifest, helloworld@xuldev.org. Set the contents of that file to be the full path to the work folder for your extension source files, C:\extensions\helloworld.

Listing 3: Additional content for overlay.xul

<?xml version="1.0"?>
<overlay id="helloworldOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <menupopup id="menu_ToolsPopup">
    <menuitem id="helloworldMenuitem" label="Hello, World!" insertbefore="sanitizeSeparator"
     oncommand="window.openDialog('chrome://helloworld/content/clock.xul','Clock','chrome,centerscreen,modal');"/>
  </menupopup>
</overlay>

 

Phase 2: Adding a function to display the time

In Phase 2, we will make it so that selecting the Hello World menu item we created in Phase 1 will display a window with the time (Figure 6).

FIXME: Figure 6: Clock window produced by Phase 2

Adding an event handler

First, we add an event handler to the menu item that will open the window (Listing 4).

FIXME: Listing 4: Event handler

Clock handler

Create the window that displays the clock (Listing 5) and its controller (Listing 6). We’ve already covered the dialog element in Chapter 3. In this case, we only want to display an OK button, so we set the buttons attribute to accept. Within the window are a label element and textbox element, wrapped in a hbox element so that they are arranged horizontally.

Listing 5: Content for clock.xul

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<dialog id="clockDialog" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    title="Clock"
    buttons="accept"
    onload="initClock();">
  <script type="application/javascript" src="chrome://helloworld/content/clock.js"/>
  <hbox align="center">
    <label value="Current Time:" />
    <textbox id="currentTime" />
  </hbox>
</dialog>

Listing 6: content for clock.js

function initClock() {
  showCurrentTime();
  window.setInterval(showCurrentTime, 1000);
}

function showCurrentTime() {
  var textbox = document.getElementById("currentTime");
  textbox.value = new Date().toLocaleTimeString();
  textbox.select();
}

Operations check

Perform an operations check to make sure that your changes to the source file are correct. At this point, it’s important that you disabled the XUL cache as discussed in the earlier section, “Setting up your development environment”. Assuming you’ve done that, you’ll be able to confirm the changes to overlay.xul and clock.xul without the bother of relaunching Firefox or reinstalling the extension. If the browser.xul file, which is the target of the overlay in overlay.xul is being read in again, the changes will be reflected, and you’ll be able to see the changes by opening a new browser window. The files clock.xul and clock.js will be read in every time you open the clock window, so just re-opening the clock window once will suffice to check them. During development, it’s important to minimize the number of steps between revising a source file and running an operations check against those changes. The part “Operations checks on revised source files” discusses the general procedure to follow from source-file revision to operations check.

What if something’s not working right?

If your extension isn’t working according to plan, first see if there’s an error being displayed on the Errors panel of the Error Console. If you set things up as recommended in the section “Setting up your development environment”, XUL and JavaScript errors will appear here. See the part “JavaScript debugging techniques” for useful tips on that subject5. If Firefox freezes up, you’ll need to terminate the Firefox process from the task manager.

 

Phase 3: Adding multilingual support

The clock window that we created in Phase 2 displays everything in English. In Phase 3, we’re going to add multilingual support, making it so that it users running Firefox in a French environment will see “Heure actuelle”, and those running it in English will see “Current time” (Figure 7).

FIXME: Figure 7: Clock window after Phase 3

Directory structure

To add multilingual support to this extension, we’ll add a locale package to the chrome. Create a new locale folder with subfolders and files within the helloworld folder, as shown in Figure 8. The purpose of each of these files is explained in Table 3.

FIXME: Figure 8: Directory structure with locale package added

Table 3: Files used in Phase 3

FIXME: Make the table cleaner

name

purpose

locale\en-US\clock.dtd

DTD definining entity references used in clock.xul (for English).

locale\fr-FR\clock.dtd

DTD definining entity references used in clock.xul (for French).

Adding locale packages

Registering locale packages (locale instruction)

The line beginning locale is used to register the chrome’s local package; helloworld is the package name, and locale/en-US/ and locale/fr-FR/ are the relative paths to the folders containing the source files; en-US and fr-FR indicate that those locale packages are for English and French, respectively.

Listing 7: Additional content for chrome.manifest

locale helloworld en-US locale/en-US/
locale helloworld fr-FR locale/fr-FR/

Replacing text with entity references in XUL

To make the extension support multiple languages, you will need to separate out all the hard-coded display text in clock.xul and move it to a DTD file. The DTD file is located inside the locale package, and the correct file is picked automatically to match the user’s language preferences. Edit clock.xul to match Listing 8. Add the DOCTYPE declaration that references the DTD file, and replace “clock” and “current time” with entity references.

Listing 8: Revisions to clock.xul

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
<!DOCTYPE dialog SYSTEM "chrome://helloworld/locale/clock.dtd">
<dialog id="clockDialog" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    title="&helloworld.clock;" buttons="accept"
    onload="initClock();">
  <script type="application/javascript" src="chrome://helloworld/content/clock.js" />
  <hbox align="center">
    <label value="&helloworld.currentTime;:" />
    <textbox id="currentTime" />
  </hbox>
</dialog>

Create the DTD that defines the entity references

Now create the DTD file that gets referred to by clock.xul (Listing 9). The clock.dtd file inside the fr-FR folder must be encoded as UTF-8.

Listing 9: Content for clock.dtd

locale\en-US\clock.dtd:

<!ENTITY helloworld.clock "Clock">
<!ENTITY helloworld.currentTime "Current Time">

locale\fr-FR\clock.dtd:

<!ENTITY helloworld.clock "Horloge">
<!ENTITY helloworld.currentTime "Heure courante">

Replacing User Interface messages within JavaScript files with properties references


It may happen your extension displays some useful messages such as alerts to the user.  Though they are nested in JavaScript, the strings should be localized too, for the sake of UI consistency.
Suppose you have these strings nested in a .js file:

if ( password == userPassword ) {
oPrefs.setBoolPref("access.authenticated", true);
}
else {
alert ("Invalid password");
......
function clear()
{
sure = confirm("Are you sure?");

First, you have to find or create a myextension.properties file in your locale folder (Notice: always stick to UTF-8). Then you simply write variable names and the sentences or words that should appear to the final user. Something like :

wrongPassMessage=Invalid password
areYouSureMessage=Are you sure?

(where Invalid password and Are you sure? are the strings to be displayed to the final user) Notice : a simple sign is enough


Now you go back to the .js file where the strings to be localized are nested.
At the very top of the file you create one string bundle with the address of the properties file where to find strings.

var gmyextensionBundle = Components.classes["@mozilla.org/intl/stringbundle;1"].getService(Components.interfaces.nsIStringBundleService);

var _bundle = gmyextensionBundle.createBundle("chrome://myextension/locale/myextension.properties")

 

You can now use your string substitutes in the .js where they are needed: see examples below

if ( password == userPassword ) {
  oPrefs.setBoolPref("access.authenticated", true);
} 
else {
  alert (_bundle.GetStringFromName("wrongPassMessage"));

function clear() {
  sure = confirm(_bundle.GetStringFromName("areYouSureMessage"));

}

Operations check

This brings us to the operations check. Because we’ve changed the chrome manifest, we need to relaunch Firefox once, as discussed in the part “Operations checks on revised source files”. So relaunch Firefox and display the clock window to make sure it is working correctly. Then type about:config into the location bar, and change the value of general.useragent.locale from en to fr, and re-display the clock window to confirm that it appears in French now.

 

Phase 4: Adding a toolbar button

In Phase 4, we’ll use graphics and style sheets to create a toolbar button that will open the clock window (Figure 9).

FIXME: Figure 9: Toolbar after Phase 4

Directory structure

The style sheet and graphics files will be located inside the chrome’s skin package. The extension will actually work fine even if these are located in the content package, but putting them in the skin package has the benefit of making it possible to design GUIs that match specific Firefox themes, and allowing theme developers to create special visual effects for extensions.

First, add the skin directory and its subordinate files to your helloworld directory as shown in Figure 10. The purpose of each of these files is explained in Table 4. Download the files icon.png and icon-small.png from the resources website and place them appropriately.

FIXME: Figure 10: Directory structure with skin package added

Add the skin package

Update chrome.manifest with the contents of Listing 10.

Listing 10: Additional content for chrome.manifest

skin helloworld classic/1.0 skin/classic/
style chrome://browser/content/browser.xul chrome://helloworld/skin/overlay.css
style chrome://global/content/customizeToolbar.xul chrome://helloworld/skin/overlay.css
Register skin package (skin instruction)

Line 1 beginning skin is used to register the skin package; helloworld is the package name; skin/classic/ is the relative path to the folder containing the source files; classic/1.0 indicates that this skin package is meant for the Firefox standard theme.

Cross-package overlays with style sheets (style instruction)

In Phase 1, we used the overlay instruction to invoke a cross-package overlay with XUL; to do this with a style sheet, we use the style instruction. Lines 2-3 create overlays on the browser window and the “customize toolbar” window.

Add the toolbar button

To add the toolbar button to the browser window, update overlay.xul as shown in Listing 11. In order to make it so that users can relocate the button where they like, you will add a new toolbarbutton element, using the special element toolbarpalette with the id BrowserToolbarPalette as the merge point. You’ll also add a new command element, so that a click on the toolbar button will share its process with the menu item.

Listing 11: Revisions to overlay.xul

<?xml version="1.0"?>
<overlay id="helloworldOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <commandset id="mainCommandSet">
    <command id="helloworldCommand" oncommand="window.openDialog(
      'chrome://helloworld/content/clock.xul',
      'Clock','chrome,centerscreen,modal');" />
  </commandset>
  <toolbarpalette id="BrowserToolbarPalette">
     <toolbarbutton id="helloworldButton" label="Hello, World!" class="toolbarbutton-1" command="helloworldCommand" />
  </toolbarpalette>
  <menupopup id="menu_ToolsPopup">
    <menuitem id="helloworldMenuitem" label="Hello, World!" insertbefore="sanitizeSeparator"
       command="helloworldCommand" />
  </menupopup>
</overlay>

Create the style sheet

Update the file overlay.css with the contents of Listing 12. Lines 1–3 define the icon image to use with full-sized icons, and lines 4–6 define the icon image to use when small toolbar icons have been selected.

Listing 12: Content for overlay.css

#helloworldButton {
  list-style-image:
  url(chrome://helloworld/skin/icon.png);
}

toolbar[iconsize="small"] #helloworldButton {
  list-style-image: url(chrome://helloworld/skin/icon-small.png);
}

Operations check

Since we have edited the chrome manifest, as we did in Phase 3, we need to relaunch Firefox. Check to make sure that, when you click on the “customize toolbar” button at the top-right of the window that the new toolbar icon appears in the resulting window. Drag it onto the toolbar, and click it to make sure it works correctly.

Table 4: Files used in Phase 4

FIXME: Make the table cleaner

name

purpose

icon.png

The full-size toolbar button icon file

icon-small.png

the small toolbar button icon file

overlay.css

Style sheet that defines the toolbar buttons to use. Overlays both the browser window and the “customize toolbar” window.

Phase 5: XPI packaging

We actually completed our Hello World extension in Phase 4, but you can’t distribute the source files in this state to other users. So we need to create an xpi-formatted installer.

XPI file directory structure

XPI is basically just a zip archive, but the directory structure inside an xpi file is generally different from your development directory structure (Figure 11).

FIXME: Figure 11: Directory structure inside an xpi file

Packaging procedure

To package your extension as an xpi while preserving the directory structure of the source files you used during development, make the changes shown in Figure 11, as described below.

  1. Create a new chrome directory inside the helloworld directory.
  2. Zip the content, locale, and skin directories1, rename the resulting archive helloworld.jar, and place it inside the chrome directory you created in Step 1.
  3. Copy chrome.manifest and rename the backup chrome.manifest.bak.
  4. Change the content of chrome.manifest to be in line with Listing 13.
  5. Zip install.rdf, chrome.manifest, and the chrome folder together, and rename the resulting archive helloworld.xpi.

Listing 13: Revisions to chrome.manifest

content helloworld jar:chrome/helloworld.jar!/content/
overlay chrome://browser/content/browser.xul chrome://helloworld/content/overlay.xul
locale helloworld en-US jar:chrome/helloworld.jar!/locale/en-US/
locale helloworld fr-FR jar:chrome/helloworld.jar!/locale/fr-FR/
skin helloworld classic/1.0 jar:chrome/helloworld.jar!/skin/classic/
style chrome://browser/content/browser.xul chrome://helloworld/skin/overlay.css
style chrome://global/content/customizeToolbar.xul chrome://helloworld/skin/overlay.css

That completes the process of XPI packaging. One point you need to take special note of is that in Step 4, you updated chrome.manifest so that the files it refers to have paths pointing inside the zip archive.

In order to go back into development mode after you’ve finished packaging the extension as an xpi, you need to revert the chrome.manifest you edited in Step 4 to point to the backup files in Step 3. If you forget to do this, any subsequent changes you make to your source files during development will not be reflected. You may want to write a batch file to automate this task.

There’s a simpler way to package an extensions as an xpi. Create a zip archive containing install.rdf, chrome.manifest, content, locale, and skin. Name this helloworld.xpi.

This omits creating the archive of the chrome packages called helloworld.jar. This kind of xpi file will run fine, but will slow down Firefox’s launch time.

XPI operations check

After you create the xpi file, make sure it works. Since you’ve already got the development version of the Hello World extension running as a test install on your current profile, restart Firefox under a different profile (see footnote 1). Drag and drop the xpi file onto your browser window to start the installation.

This completes the explanation of how to create and package a simple “hello world” extension.

Developing practical extensions:
A session-management extension

In this section, we will create an extension that uses a new feature: the session store API. This will allow the user to save and restore session snapshots (browser window states) at any time.

Phase 1: test install

Figure 12 shows what the interface for the session-management extension will look like. Under the Tool menu, the Session Store submenu contains two items—Save Session and Clear Sessions; in between them is a list of previously saved sessions that you can restore, most recent first.

FIXME: Figure 12: Session-management extension interface: the menu

As discussed in the part “The Session Store API”, sessions are represented as JSON character strings. Sessions are stored in a subdirectory of the profile directory (called “sessionstore”), with each session stored as a text file in it (Figure 13).

FIXME: Figure 13: Schematic of session saving and restorin

Source file structure

FIXME: Figure 14 shows the directory structure for this project

Creating install manifest

Create your work folder, and create an install manifest with the contents of Listing 1, but in this case, change em:id to sessionstore@example.org and em:name to Session Store.

Creating chrome manifest

Create a chrome manifest with the contents of Listing 2.

Finding overlay merge points

Use the same overlay merge point and element to add on to as you did for the Hello World extension.

Browser window overlay

Create a file that will overlay the browser window, overlay.xul, with the contents from Listing 14.

Listing 14: Content for overlay.xul

<?xml version="1.0"?>
<overlay id="sessionstoreOverlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/javascript"
      src="chrome://sessionstore/content/overlay.js" />
    <menupopup id="menu_ToolsPopup">
      <menu label="Session Store" insertbefore="sanitizeSeparator">
        <menupopup>
          <menuitem label="Save Session" />
          <menuseparator />
          <!-- Dynamically generated menu items go here -->
          <menuseparator />
        <menuitem label="Clear Sessions" />
      </menupopup>
    </menu>
  </menupopup>
</overlay>

Test install

Perform a test install using pointer files as you did in the previous section. Afterwards, you should see a Session Store menu item under the Tools menu.

 

Phase 2: Implement functionality

In Phase 2, we’ll use JavaScript to implement the session save and restore functions using the session store API.

Figure out JavaScript skeleton

Decide on method names and variable names for each of the processes you’ll need to implement these features, and put them into an overlay.js file. Follow the code in Listing 15.

Listing 15: Content for overlay.js

var gSessionStore = {
  // Directory to save sessions (nsILocalFile)
  _dir: null,
  // Initialization
  init: function() { },
  // uninitialization
  uninit: function() { },
  // Save session (event handler)
  save: function(event) { },
  // Restore session (event handler)
  restore: function(event) { },
  // Delete session (event handler)
  clear: function(event) { },
  // Dynamically generate menu items (event handler)
  createMenu: function(event) { },
  // Read file
  _readFile: function(aFile) { },
  // Write file
  _writeFile: function(aFile, aData) { },
};
window.addEventListener("load", function(){
gSessionStore.init(); }, false);
window.addEventListener("unload", function(){
gSessionStore.uninit(); }, false);
Wrap methods and variables as properties of objects

You’ll note that in Listing 15, I’ve defined a lot of different methods and variables as properties of a single object, gSessionStore. Take note that when using an extension to create an overlay on the browser window, any global variables and functions defined within JavaScript that get opened as overlays on top of browser.xul all become properties of the browser.xul window object. This means that all those variables and functions get mixed up with those defined by Firefox itself and any other extensions that are running. Wrapping everything in one object is a good way to isolate extensions from each other and keep them from trampling each others’ toes. Defining a class would be another good way.

Initializing when opening browser window

With extensions, there are a lot of situations where you want to perform some kind of initialization when the browser window opens. To do that, add an event listener for the load event that targets the window object. In Listing 15, an init method runs when the window opens, and an uninit method runs when the window closes.

Adding event handlers

Add the four event handlers you defined in overlay.js to overlay.xul (Listing 16). This takes advantage of event bubbling2, appending the restore event handler to the menupopup element one layer above the menuitem element. In the code for the save and restore event handlers, we will actually need blocks to prevent event bubbling.

Listing 16: Revisions to overlay.xul

<menupopup onpopupshowing="gSessionStore.createMenu(event);"
    oncommand="gSessionStore.restore(event);">
  <menuitem label="Save Session" oncommand="gSessionStore.save(event);" />
  <menuseparator />
  <!-- Dynamically generated menu items go here -->
  <menuseparator />
  <menuitem label="Clear Sessions" oncommand="gSessionStore.clear(event);" />
</menupopup>

 

Implementing methods

Now we’re going to actually implement the methods we defined in the gSessionStore object, one at a time. In the interest of space, I’m not including all the code here, but you can download it from the following URL: FIXME: include code from xuldev: translate it and attach it to the chapter document

init method

The init method is as shown in Listing 17. The init method first gets the sessionstore directory inside the profile directory using nslLocalFile, which we’ll refer to through the variable _dir in subsequent lines (lines 3–6); if the directory doesn’t exist, we’ll create it (lines 7–8)3.

Listing 17: Content for init method

init: function() {
  var dirSvc = Components.classes["@mozilla.org/file/directory_service;1"]
               .getService(Components.interfaces.nsIProperties);
  this._dir = dirSvc.get("ProfD", Components.interfaces.nsILocalFile);
  this._dir.append("sessionstore");
  if (!this._dir.exists())
    this._dir.create(this._dir.DIRECTORY_TYPE, 0700);
},
uninit method

Listing 18 gives the code for the uninit method. This just discards the variable _dir that was created by the init method. This process actually isn’t important.

Listing 18: Content for uninit method

uninit: function() {
  this._dir = null;
},
save method

Listing 19 gives the code for the save method. In line 3, you see the block that prevents the bubbling that would result in this being called by the oncommand handler in the menupop element, which is the parent of the menuitem element. In lines 4–6, we use the nslSessionStore interface’s getBrowserState method to get a JSON text-string representation of the states of all currently open browser windows. In lines 7–9, we create the target file’s nslFile object by duplicating the _dir property. In line 10, we use the _writefile method (which we’ll get to shortly) to write the JSON string representing the session to the file.

Listing 19: Content for save method

save: function(event) {
  event.stopPropagation();
  var ss = Components.classes["@mozilla.org/browser/sessionstore;1"]
           .getService(Components.interfaces.nsISessionStore);
  var state = ss.getBrowserState();
  var fileName = "session_" + Date.now() + ".js";
  var file = this._dir.clone();
  file.append(fileName);
  this._writeFile(file, state);
},
restore method

Listing 20 shows the event handler for the dynamically generated menu items, which were created using the createMenu method (which we’ll get to shortly). Lines 3–6 get the file name from the special attribute fileName that was added by the createMenu method, and read in the JSON text string from that file using the _readFile method (which we’ll get to shortly). Lines 7–9 use the nslSessionStore interface’s setWindowState method to restore the session, using the current window as an origin point. Session restoration overwrites any currently open tabs.

Listing 20: Content for restore method

restore: function(event) {
  var fileName = event.target.getAttribute("fileName");
  var file = this._dir.clone();
  file.append(fileName);
  var state = this._readFile(file);
  var ss = Components.classes["@mozilla.org/browser/sessionstore;1"]
           .getService(Components.interfaces.nsISessionStore);
  ss.setWindowState(window, state, false);
},
clear method

This uses the directoryEntries property of the nslFile interface to delete all files within the directory you got earlier. Refer to the section “Traversing folders” in Chapter 4.

Listing 21: Content for clear method

clear: function(event)
  {
    event.preventBubble();
    var fileEnum = this._dir.directoryEntries;
    while (fileEnum.hasMoreElements()) {
      var file = fileEnum.getNext().QueryInterface(Components.interfaces.nsIFile);
      file.remove(false);
      // debug
      dump("SessionStore> clear: " + file.leafName + "\n");
    }
  },

 

_readFile method

Reads the nslFile object passed as a parameter, and returns its contents as a text string. When the file is read in, its contents are converted from UTF-8 to Unicode.

Listing 22: Content for _readFile method

_readFile: function(aFile)
  {
    try {
      var stream = Components.classes["@mozilla.org/network/file-input-stream;1"].
                   createInstance(Components.interfaces.nsIFileInputStream);
      stream.init(aFile, 0x01, 0, 0);
      var cvstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
                     createInstance(Components.interfaces.nsIConverterInputStream);
      cvstream.init(stream, "UTF-8", 1024, Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
      var content = "";
      var data = {};
      while (cvstream.readString(4096, data)) {
        content += data.value;
      }
      cvstream.close();
      return content.replace(/\r\n?/g, "\n");
    }
    catch (ex) { }
    return null;
  },
_writeFile method

Creates a new file for the nslFile object passed as a parameter, and writes the text string, also passed as a parameter. Upon writing, the text is converted from Unicode to UTF-8.

Listing 23: Content for _writeFile method

_writeFile: function(aFile, aData)
  {
    // init stream
    var stream = Components.classes["@mozilla.org/network/safe-file-output-stream;1"].
                 createInstance(Components.interfaces.nsIFileOutputStream);
    stream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
    // convert to UTF-8
    var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
                    createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";
    var convertedData = converter.ConvertFromUnicode(aData);
    convertedData += converter.Finish();
    // write and close stream
    stream.write(convertedData, convertedData.length);
    if (stream instanceof Components.interfaces.nsISafeOutputStream) {
      stream.finish();
    } else {
      stream.close();
    }
  },
createMenu method

This event handler is called when the Session Store submenu opens. First, it deletes the menuitem elements that were dynamically generated the last time the submenu opened, and which are still left over. Next, it dynamically generates menuitem elements based on the names of all the files in the session-storage directory, and inserts them into the menu.

Listing 24: Content for createMenu method

 createMenu: function(event)
  {
    var menupopup = event.target;
    for (var i = menupopup.childNodes.length - 1; i >= 0; i--) {
      var node = menupopup.childNodes[i];
      if (node.hasAttribute("fileName"))
        menupopup.removeChild(node);
    }
    var fileEnum = this._dir.directoryEntries;
    while (fileEnum.hasMoreElements()) {
      var file = fileEnum.getNext().QueryInterface(Components.interfaces.nsIFile);
      var re = new RegExp("^session_(\\d+)\.js$");
      if (!re.test(file.leafName))
        continue;
      var dateTime = new Date(parseInt(RegExp.$1, 10)).toLocaleString();
      var menuitem = document.createElement("menuitem");
      menuitem.setAttribute("label", dateTime);
      menuitem.setAttribute("fileName", file.leafName);
      menupopup.insertBefore(menuitem, menupopup.firstChild.nextSibling.nextSibling);
    }
  },
Operations check

This completes the functional implementation. Open a new window and make sure that all three functions—saving a session, restoring a session, and deleting sessions—all work correctly.

 

Phase 3: Creating a preference panel

You don’t need to use ini files, registries, or anything like that to give users options they can set through a preference panel. Firefox lets you read and write preference values using handy XPCOM services and create preference panels with interface widgets. In Phase 3, we’ll try this out (Figure 15).

FIXME: Figure 15: Finished preference panel

Directory structure

Figure 16 shows the directory structure we’ll be using in Phase 3. Table 5 explains the new files being used.

Table 5: Files used in Phase 3

FIXME: Make the table cleaner

name

purpose

sessionstore-prefs.js

File sets defaults for preferences. This should always be located inside defaults\preferences

prefs.xul

The preference panel

 FIXME: Figure 16: Source file structure

Deciding the format for your preferences

Next you need to determine the names, types, and values for your preferences (Table 6). Each preference should be prefixed with something that uniquely identifies your extension—in this case, we’ll use extensions.sessionstore.

Table 6: Preference formats

FIXME: Make the table cleaner

Preference name

Type

Value

extensions.sessionstore.warnOnClear

Boolean

Activated when deleting sessions. If true, causes a confirmation dialog to appear; if false, deletes immediately.

extensions.sessionstore.replaceTabs

Integer

Activated when restoring sessions.

0: Leave current tabs

1: Overwrite current tabs

2: Ask each time

Defaults definition file

Listing 25 gives the contents for the default preferences, as described in Table 6. Save this in defaults\preferences\sessionstoreprefs.js.

Listing 25: Content for sessionstore-prefs.js

pref("extensions.sessionstore.warnOnClear", true);
pref("extensions.sessionstore.replaceTabs", 2);

Creating the preference panel

Create prefs.xul with the contents from Listing 26. The prefwindow element is a special root element for creating preference panels, with the prefpane element subordinate to it. You can create an interface similar to Firefox’s options window by inserting multiple prefpane elements, which allows users to switch between panels by clicking buttons. The preference element creates a preference value that can be written and read by the preference panel, with the preference name indicated by the name attribute, and the type by the type attribute. Furthermore, by matching the preference element’s id to the preference attribute of a checkbox or radiogroup, you can specify the interface widget that will be used to set your preferences. Add the contents of Listing 27 to your install manifest to make it so that the Add-on Manager can open your preference panel.

Listing 26: Content for prefs.xul

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<prefwindow id="sessionstorePrefs" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
     title="Session Store Preferences">
  <prefpane>
    <preferences>
      <preference id="extensions.sessionstore.warnOnClear"
           name="extensions.sessionstore.warnOnClear"
           type="bool" />
      <preference id="extensions.sessionstore.replaceTabs"
           name="extensions.sessionstore.replaceTabs"
           type="int" />
    </preferences>
    <checkbox label="Confirm before clearing sessions"
         preference="extensions.sessionstore.warnOnClear" />
    <groupbox>
      <caption label="When restoring session:" />
      <radiogroup preference="extensions.sessionstore.replaceTabs">
        <radio value="0" label="Keep current tabs" />
        <radio value="1" label="Replace current tabs"/>
        <radio value="2" label="Ask me every time" />
      </radiogroup>
    </groupbox>
  </prefpane>
</prefwindow>

Listing 27: Additional content for install.rdf

<em:optionsURL>chrome://sessionstore/content/prefs.xul</em:optionsURL>

Changing behavior through preference values

In Listing 28, we add a preference that allows a confirmation dialog to appear when we delete sessions. When we get the preference value from JavaScript, we take advantage the XPCOM nslPrefService interface, using the getBoolPref and getIntPref methods as appropriate to the data type. In Listing 28, we get the nslPrefBranch object that lets us read and write all preferences beginning with the prefix extensions.sessionstore., which is what lets us use the getBoolPref method to get the value.

FIXME: Listing 28: nslPrefBranch object demonstration

Operations check

We’ve revised the install manifest, so we need to reinstall the extension (see the part “Operations checks on revised source files”). Once you’ve reinstalled it, first open about:config, and confirm that the previous preference values are set. Next, select Session Store from the Add-on Manager and click the “Preferences” button to bring up the preference panel. Try changing the preferences, and see if those changes are reflected when you restore or delete sessions.

Phase 4: XPI packaging

This procedure is unchanged from Phase 5 in the “Developing a Simple Extension” section, but take note of everything within the defaults folder.

Conclusion

We’ve looked at how to build extensions at an extremely basic level in this chapter, and there are a lot of other interesting features to explore in extensions and XUL. But I hope that having gotten your feet wet with extensions development, you’ll want to dive in now.

Operations checks on revised source files

In the interest of developing extensions efficiently, you want to minimize the number of steps between revising a source file and running an operations check on it. For each type of source file, there’s a different procedure to follow. The following all assumes that you’ve disabled the XUL cache as discussed in the section “Setting up your development environment”.

XUL that opens new windows

Confirm changes by closing and reopening window. If the XUL is displaying as a sidebar, reopen the sidebar. Likewise with any JavaScript, DTDs, or style sheets referenced by the XUL.

XUL overlaying other windows

You need to force the overlay-target XUL to reload. If it’s the browser window that’s the target, you can simply open a new browser window to confirm the changes.

properties file

If you make changes to a properties file in the locale package, those changes will not be reflected in Firefox until you relaunch it.

XPCOM components inside the components directory

You will need to delete the files compreg.dat and xpti.dat inside the profile folder and relaunch Firefox.

Default preferences file inside the defaults\preferences directory

You will need to relaunch Firefox.

chrome.manifest

You will need to relaunch Firefox.

install.rdf

You will need to temporarily uninstall the extension and then reinstall it. In practice, what you’ll do is remove the pointer file, relaunch Firefox, return the pointer file, and relaunch Firefox again. On a Linux system, use the touch command on the directory pointed at by the pointer file to change its “last changed” date, and avoid the relaunching.

The session store API

The session store API4 is one of the new developer-oriented features in Firefox 2. Using the various methods in the nslSessionStore interface, which is an XPCOM service, you can easily save and restore session states. Functions in Firefox that let you reopen the last closed tab, or restore to your previous state after a crash, are all implemented through this API.

We used two methods in the nslSessionStore interface in this project. Let’s look at how they work.

getBrowserState()

Returns a JSON-formatted5 text string representing the state of every currently open browser window. This string includes extensive information regarding every open window and tab, the forward/back history for each tab, page scroll position, text zoom, contents of form elements, etc.

setWindowState(aWindow, aState, aOverwrite)

Restores the browser window indicated by the parameter aWindow to the state indicated by aState. If aOverwrite is true, the currently open tab is overwritten; otherwise, a new tab is created for the restored content.

 

JavaScript debugging methods

alert

The easiest way to debug javascvript is using the window.alert method to display variables in a dialog (Listing 29). Except for asynchronous processes, all processing stops while the dialog is up, so this technique is useful when you want to pin down a value that can vary during a process.

Listing 29: Check the value of variable foo

alert(foo);

dump

In a Windows environment, you can launch Firefox with the -console argument, which opens a dump console. Using the dump method, you can send text to the dump console (Listing 30). The text output being sent to the console does not suspend any other processes, and this is most useful when there’s a high volume of messages being output. In order to use the dump method most effectively, you’ll need to follow the recommendations in “Setting up your development environment”. Note that in a Windows environment, non-latin text sent to the dump console will get corrupted.

Listing 30: List the properties of obj

for (var i in obj) {
  dump(i + " : " + obj[i] + "\n");
}

error console

The nslConsoleService interface

Using the logStringMessage method of the nslConsoleService interface, which is an XPCOM service, you can output text to the “message” panel of the error console. Non-latn text will appear correctly here. If you are going to be issuing messages to the error console frequently, it’s handy to define a function like that in Listing 31.

Listing 31: Function to output messages to the error console

function log(aText) {
  var console = Components.classes["@mozilla.org/consoleservice;1"]
      .getService(Components.interfaces.nsIConsoleService);
  console.logStringMessage(aText);
}
Components.utils.reportError method

Another way to output text to the same error console is with the Components.utils.reportError method. This will send its output to the “error” panel of the error console.

 

 

Where can I learn more about the XPCOM interface?

FIXME: add links:

The Mozilla Development Center has extensive documentation on the nsISessionStore interface we’ve discussed in this section6. In general, details (IDL) on the XPCOM interface are available from the Mozilla Cross-Reference7, which includes source code with full-text searching available.

1 - In Windows XP, you can use the “compress folder” command to create a zip archive, but I recommend installing a tool like 7-Zip that lets you specify the compression ratio. It’s OK to have a low compression ratio when you are creating the jar file, and a high compression ratio when creating the xpi file.
2 - An event generated by an element “bubbles up” to the root.
3 - See Code Snippets: File I/O-MDC (http://developer.mozilla.org/en/docs..._special_files) for techniques on getting special folders, like the profile folder.
5 - JSON is short for JavaScript Object Notation. It is a data format that can easily be read from and written to in JavaScript.
6 - nslSessionStore-MDC (http://developer.mozilla.org)
7 - Mozilla Cross-Reference (http://mxr.mozilla.org/)

Document Tags and Contributors

Last updated by: Sheppy,