Add-ons using the techniques described in this document are considered a legacy technology in Firefox. Don't use these techniques to develop new add-ons. Use WebExtensions instead. If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions.
From Firefox 53 onwards, no new legacy add-ons will be accepted on addons.mozilla.org (AMO).
From Firefox 57 onwards, WebExtensions will be the only supported extension type, and Firefox will not load other types.
Even before Firefox 57, changes coming up in the Firefox platform will break many legacy extensions. These changes include multiprocess Firefox (e10s), sandboxing, and multiple content processes. Legacy extensions that are affected by these changes should migrate to WebExtensions if they can. See the "Compatibility Milestones" document for more.
A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.
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.
- An extension’s GUI, or chrome, can consist of three kinds of package: content, locale, and skin.
- Use a cross-package overlay on top of browser.xul to add a button or menu item to the Firefox browser window.
- 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.
Figure 4: Folder structure used in Phase 1
C:/ └───extensions └───helloworld │ chrome.manifest │ install.rdf │ └───content clock.js clock.xul overlay.xul
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).
- Open the DOM Inspector by selecting the Tools > DOM Inspector menu item. Firefox 31 Open the DOM Inspector by open menu>Developer>DOM Inspector menu item.
- 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.
- Click the spots numbered 1 and 2 in Figure 5, in that order; the menu element (3) will then be selected.
- 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.
- 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
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
File name | Role |
---|---|
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.
- Create a new chrome directory inside the helloworld directory.
- 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.
- Copy chrome.manifest and rename the backup chrome.manifest.bak.
- Change the content of chrome.manifest to be in line with Listing 13.
- 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.