Chapter 4: Using XPCOM—Implementing advanced processes

Draft
This page is not complete.

FIXME: We should include a link to the MDC list of snippets

FIXME: We need to add a part about 'why and how to create your own component' C++/JS

This document was authored by Hiroshi Shimoda of Clear Code Inc. and was originally published in Japanese for the Firefox Developers Conference Summer 2007. Mr. Shimoda is a co-author of Firefox 3 Hacks (O'Reilly Japan, 2008).

This chapter explains how to use XPCOM to implement advanced processes using only JavaScript.

Introduction

JavaScript lacks functions for opening files and character-code conversion, among other things. A different mechanism is needed to perform these functions. Internet Explorer handles this using ActiveX; in Firefox, we use the Cross-Platform Component Object Model, or XPCOM.

Due to deprecation of enablePrivilege this functionality can not be used in web pages. EnablePrivilege is disabled in Firefox 15 and will be removed in Firefox 17.

About XPCOM

XPCOM is a framework for developing platform-independent components. Components developed in line with that framework are referred to as XPCOM components, and sometimes the components are simply referred to as XPCOMs.

Firefox itself includes a great number of XPCOM components, and they can be used in extensions as well. Sometimes an extension will be packaged with a special XPCOM component developed specifically for it.

Note: If you're developing components in C++ or other compiled languages, be sure to include binaries for every platform.

Reference materials

To get an idea of what kinds of functions embedded XPCOM can handle, take a look at the API reference and the interface definitions from XPIDL in the actual source code.

Note: Interface Definition Language (IDL) is a language for giving standard definitions of objects, methods, and so forth. The XPIDL (Cross-Platform IDL) used by Mozilla is a partial extension to the CORBA IDL.

You can perform a full-text search of the Firefox source code in Mozilla Cross-Reference using character strings, filenames, etc as search keys. If you’re having trouble with any of the details of the interface, it is helpful to search on the source code for usage examples within Firefox.

Note: To look at the Firefox 3 source code, choose "Firefox 3" in the Starting Points list. For Firefox 3.5 choose "Mozilla 1.9.1." For the current development trunk of Firefox, choose "Mozilla Central" and for Thunderbird, choose "Comm. Central".

Calling XPCOM from XPConnect

Use the XPConnect technology to use XPCOM in JavaScript. Listing 1 shows how you can use XPConnect to acquire references to XPCOM services and create new XPCOM objects.

Each component is identified with a contract ID in the form @domain_name/module_name/component_name;version_number, and implements one or more interfaces that determine what functions can be called on these components. Interfaces names usually have the forms nsIxxx. In order to have access to the corresponding functions, it is necessary to use the component with the interface you want to use. Some XPCOM components are services, that means only one instance in memory. For instance, this is the case for the bookmarks component, which is actually a service. It lets you access and manipulate the user's bookmarks. Those should be accessed with getService(). For other components, you can create as many instances as you need. For instance, this is the case for files (nsILocalFile). Those are created with createInstance(). It's important to know whether a component should be created with getService() or createInstance(), because using one instead of the other can cause problems.

Listing 1: Calling XPCOM functions using XPConnect

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/javascript"><![CDATA[
   var ioService = Components.classes['@mozilla.org/network/io-service;1']
                   .getService(Components.interfaces.nsIIOService);
   alert(ioService);
  ]]></script>
</page>

Calling XPConnect using local files

Try saving the contents of Listing 1 as the file test.xul, somewhere on your desktop, and drag and drop it into Firefox to open it. You'll note that even though the file includes an alert method, nothing happens. This is a by-product of the fact that test.xul currently doesn’t have privileges.

In order to use XPConnect, the file needs special UniversalXPConnect privileges. Because ordinary web pages and local files don't have privileges, it's impossible to actually try out the sample code in this chapter.

In order to set UniversalXPConnect privileges, you need to run the code in Listing 2.

Listing 2: Setting privileges

netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
Note: This is unneeded when the code is part of an extension, and will result in rejection if submitted to addons.mozilla.org.

Permit by dialog

Try adding the contents of Listing 2 to test.xul, before the line var ioService = …, and re-open it in Firefox. You should now see the confirmation dialog shown in Figure 1. Pressing the "Permit" button grants UniversalXPConnect permission in this execution context, making it possible to run XPConnect temporarily. An XPConnect-wrapped nsIIOService message should appear.

If you check β€œapply these privileges in the future”, all local files will be able to run XPConnect without confirmation in the future. This is extremely dangerous, and you should never check this option.

Editing prefs.js

Opening test.xul will produce the dialog shown in Figure 1. This can get annoying. To run the file's scripts without manual confirmation, add the contents of Listing 3 to the prefs.js file in the user profile folder. Opening test.xul will show the local file URL in the location bar. Copy and paste this into the appropriate spot in Listing 3.

Note: The location of the user profile will vary depending on your system. On Windows Vista, it will be located at C:\Users\username\AppData\Roaming\Mozilla\Firefox\Profiles\random number.default\ ; On Windows XP or 2000, it will be C:\Documents and Settings\username\Application Data\Mozilla\Firefox\Profiles\random number.default\ ; On Linux, it will be ~/.mozilla/firefox/random number.default/ ; On Mac OS X, it will be ~/Library/Application Support/Firefox/Profiles/random number.default/

In the interests of security, delete these lines from prefs.js after finishing these tests.

Note that Firefox itself and its extensions have privileges set by default after installation and registration. Extensions don't need special code like that in Listing 2 in order to get privileges.

FIXME: We should advise against using such method

FIXME: Figure 1: Dialog requesting privileges

Listing 3: Grant privileges without manual confirmation for a specific file

user_pref("capability.principal.codebase.test.granted", "UniversalXPConnect");
user_pref("capability.principal.codebase.test.id", "File URL");

Frequently used XPCOM functions

Let’s take a look at those XPCOM functions that are used especially frequently. You should be able to get the gist of how XPCOM is used by looking at the sample code. You can paste almost all the sample code in this chapter into your existing test.xul file to try it out. Feel free to fill in blank spots as you see fit. Make sure to change file paths to match your own environment.

You can download the source code used in this chapter.

Get window

While you can use JavaScript to get child windows opened from the parent window, you cannot get dialogs or windows that have no relation to that window. To overcome this limitation, nsIWindowMediator makes it possible to access all of Firefox's windows.

Get active window

One thing that nsIWindowMediator is often used for is to get the active window. Listing 4 shows how to get the active browser window and the number of open tabs.

Passing the name of the window type as the parameter of the nsIWindowMediator.getMostRecentWindow() method returns the most recently active window from among the root element's windows with that type set for the windowtype attribute value. Setting the parameter to null returns the active window from among all windows, including dialogs, etc.

Listing 4: Get active browser window

netscape.security.PrivilegeManager
	  .enablePrivilege('UniversalXPConnect');

var WindowMediator = Components
			.classes['@mozilla.org/appshell/window-mediator;1']
			.getService(Components.interfaces.nsIWindowMediator);
var browser = WindowMediator.getMostRecentWindow('navigator:browser');
alert(browser.gBrowser.mTabs.length);

Get overview of all windows with a certain type

Use the nsIWindowMediator.getEnumerator() method to get an overview of all windows that have a certain type. Listing 5 shows how to get a summary of all browser windows in Firefox and then close them.

Listing 5: Closing all browser windows

var browsers = WindowMediator.getEnumerator('navigator:browser');
var browser;
while (browsers.hasMoreElements()) {
  browser = browsers.getNext().QueryInterface(Components.interfaces.nsIDOMWindowInternal);
  browser.BrowserTryToCloseWindow();
}

This method returns an overview of the specified window type in the form of an Iterator pattern object called nsISimpleEnumerator. After getting an element with the nsISimpleEnumerator.getNext() method, use the QueryInterface method to get the interface, which allows you to handle each element as a window object.

Like the getMostRecentWindow() method, passing null as the parameter for the getEnumerator() method enables you to get all windows in Firefox, including dialogs, etc.

Manipulating files using XPCOM

XPCOM provides a number of interfaces allowing you to perform file manipulations without concern for whether you are running on Windows, Mac OS X, or Linux.

In order to work with local files, first you need to create a nsILocalFile object representing the local file, as shown in Listing 6. When you initialize this by passing the full path to the file with the nsILocalFile.initWithPath() method, it becomes available to all functions. It doesn't matter whether a file at the specified path already exists.

Note: Use the path format suited to your platform: the Windows β€œ\” path delimiter is interpreted as an escape code, so should always be written β€œ\\”; characters like β€œ./” on Linux require no special handling.

Listing 6: Creating an XPCOM object representing a file

var file = Components.classes['@mozilla.org/file/local;1']
           .createInstance(Components.interfaces.nsILocalFile);
file.initWithPath('C:\\temp\\temp.txt');

Creating and deleting files

Listing 7 shows how to delete a file if it exists, and create a new file with the same name.

Passing true as a parameter for the nsILocalFile.remove() method will recursively delete folders and files. Let's see what happens when we pass true to delete all the contents of a folder.

Listing 7: Check that file exists, delete it, and create it

file.initWithPath('C:\\temp\\temp.txt');
if (file.exists())
  file.remove(false);
file.create(file.NORMAL_FILE_TYPE, 0666);

The first parameter of the nsILocalFile.create() method gives the type of file to create. This is defined using constant properties—to create a normal file, use NORMAL_FILE_TYPE, to create a folder use DIRECTORY_TYPE. The second parameter gives the access privileges to that file using Unix-style octal values.

Note: Windows ignores the privileges parameter; other platforms may do so as well.

The nsILocalFile object includes methods that return virtual state values for the current file, as shown in Table 1.

Table 1: Methods for checking file states

Method name Description
nsILocalFile.exists() Determines whether or not the file exists.
nsILocalFile.isWriteable() Determines whether or not the file can be written to.
nsILocalFile.isReadable() Determines whether or not the file can be read.
nsILocalFile.isExecutable() Determines whether or not the file can be executed.
nsILocalFile.isHidden() Determines whether or not the file is hidden.
nsILocalFile.isDirectory() Determines whether or not the reference is to a directory.
nsILocalFile.isFile() Determines whether or not the reference is to a file.
nsILocalFile.isSymlink() Determines whether or not the reference is to a symlink.

Traversing directories

Moving into a directory

Use the nsILocalFile.append() method to drill down into a directory (or file). See Listing 8.

Listing 8: Traversing directories

file.initWithPath('C:\\');
file.append('Documents and Settings');
file.append('All Users');
file.append('Documents');

List files in specified directory

Use the directoryEntries property to perform operations on all the files or folders in a given folder. This property returns a nsISimpleEnumerator-type object similar to the window overview, so you can get each element using similar techniques. Listing 9 shows how to list the contents of a folder.

Listing 9: Listing the contents of a specific directory

file.initWithPath('C:\\');
var children = file.directoryEntries;
var child;
var list = [];
while (children.hasMoreElements()) {
  child = children.getNext().QueryInterface(Components.interfaces.nsILocalFile);
  list.push(child.leafName + (child.isDirectory() ? ' [DIR]' : ''));
}
alert(list.join('\n'));

Get parent directory

Although the nsILocalFile object does not contain a function for moving to higher directories, Listing 10 does show how you can use the parent property to get the parent directory of the current file.

Listing 10: Creating a backup of a specific file in a separate folder

file.initWithPath('C:\\temp\\temp.txt');
backupFolder = file.parent.parent; // C:\
backupFolder.append('backup'); // C:\backup
backupFolder.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0666);
file.copyTo(backupFolder, file.leafName+'.bak');

Converting between file paths and file URLs

XPCOM functions can use both remote resources and local files, and these functions almost always specify their targets using URIs. Local file paths can be converted to file URLs, such as file:///C/temp/temp.txt, as shown in Listing 11. Listing 12 shows the opposite conversion.

Listing 11: Converting a local file path to a URL

var path = 'C:\\temp\\temp.txt';
var file = Components.classes['@mozilla.org/file/local;1']
           .createInstance(Components.interfaces.nsILocalFile);
file.initWithPath(path);
var ioService = Components.classes['@mozilla.org/network/io-service;1']
                .getService(Components.interfaces.nsIIOService);
var url = ioService.newFileURI(file);
var fileURL = url.spec;
alert(fileURL); // "file:///C:/temp/temp.txt"

Listing 12: Converting a URL to a local file path

var url = 'file:///C:/temp/test.txt';
var ioService = Components.classes['@mozilla.org/network/io-service;1']
                .getService(Components.interfaces.nsIIOService);
var fileHandler = ioService.getProtocolHandler('file')
                  .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
var file = fileHandler.getFileFromURLSpec(url);
var path = file.path;
alert(path); // "C:\temp\temp.txt"

Binary file I/O

Use streams, as in Java, for file I/O in XPCOM.

Opening binary files

Listing 13 shows how to get a files contents as a bytestring (where 1 byte = an array of 8 bits). Save a text file containing only the ASCII characters "XUL" and put in the path to that file. Running this code should produce the output "58 55 4C" so you can check your results.

Note: Here, we assume that the file is named temp.txt and is saved in the temp folder on the C: drive.

Listing 13: Reading the contents of a binary file

file.initWithPath('C:\\temp\\temp.txt');
var fileStream = Components.classes['@mozilla.org/network/file-input-stream;1']
                 .createInstance(Components.interfaces.nsIFileInputStream);
fileStream.init(file, 1, 0, false);
var binaryStream = Components.classes['@mozilla.org/binaryinputstream;1']
                   .createInstance(Components.interfaces.nsIBinaryInputStream);
binaryStream.setInputStream(fileStream);
var array = binaryStream.readByteArray(fileStream.available());
binaryStream.close();
fileStream.close();
alert(array.map(
        function(aItem) {return aItem.toString(16); }
      ).join(' ').toUpperCase(
));

When we initialized nsIFileInputStream, we set the second and third parameters to initialize it in read-only mode. Once the process is complete, you should close all streams.

Outputting binary files

Listing 14 shows the opposite operation, taking a string of bytes and outputting them as a binary file. Here, we're outputting a text file consisting of the ASCII characters "XUL."

When we initialized nsIFileInputStream, we set the second and third parameters to initialize it in write-only mode. Here again, once the process is complete, you should close all streams.

Listing 14: Writing a binary file

var array = [88, 85, 76];
file.initWithPath('C:\\temp\\temp.txt');
if (file.exists())
  file.remove(true);
file.create(file.NORMAL_FILE_TYPE, 0666);
var fileStream = Components.classes['@mozilla.org/network/file-output-stream;1']
                 .createInstance(Components.interfaces.nsIFileOutputStream);
fileStream.init(file, 2, 0x200, false);
var binaryStream = Components.classes['@mozilla.org/binaryoutputstream;1']
                   .createInstance(Components.interfaces.nsIBinaryOutputStream);
binaryStream.setOutputStream(fileStream);
binaryStream.writeByteArray(array , array.length);
binaryStream.close();
fileStream.close();

Text file I/O

Text files are read the same way streams are. If the text includes multi-byte characters, as is used in Japanese, you'll need to convert the character codes.

Text file input

Listing 15 shows an example of opening a text file encoded as Shift-JIS (a double-byte character encoding for Japanese). Copy some Japanese text into a text file, save it as Shift-JIS, and put in the path to that file. The text will be displayed as-is. The text that you've read in is actually being represented internally as Unicode (UTF-16).

Listing 15: Reading a Shift-JIS text file

file.initWithPath('C:\\temp\\temp.txt');
var charset = 'Shift_JIS';
var fileStream = Components.classes['@mozilla.org/network/file-input-stream;1']
                 .createInstance(Components.interfaces.nsIFileInputStream);
fileStream.init(file, 1, 0, false);
var converterStream = Components.classes['@mozilla.org/intl/converter-input-stream;1']
                      .createInstance(Components.interfaces.nsIConverterInputStream);
                      converterStream.init(fileStream, charset, fileStream.available(),
                      converterStream.DEFAULT_REPLACEMENT_CHARACTER);
var out = {};
converterStream.readString(fileStream.available(), out);
var fileContents = out.value;
converterStream.close();
fileStream.close();
alert(fileContents);

Outputting text files

FIXME: Not sure this example is relevant in an English context, maybe something from the snippets

Listing 16 shows how to take text internally represented as Unicode and output it to a file encoded using EUC-JP (a Japanese text encoding). Here, the character string to be written, ε€‰ζ›γƒ†γ‚Ήγƒˆ, is hard-coded directly into the JavaScript source using escaped Unicode entities. Open the output file to check your results.

Note that if you set the character encoding to null it will default to UTF-8 for both input and output.

Listing 16: Writing text to a file encoded as EUC-JP

var string = '\u5909\u63db\u30c6\u30b9\u30c8';
file.initWithPath('C:\\temp\\temp.txt');
file.create(file.NORMAL_FILE_TYPE, 0666);
var charset = 'EUC-JP';
var fileStream = Components
.classes['@mozilla.org/network/file-output-stream;1']
.createInstance(Components.interfaces.nsIFileOutputStream);
fileStream.init(file, 2, 0x200, false);
var converterStream = Components
.classes['@mozilla.org/intl/converter-output-stream;1']
.createInstance(Components.interfaces.nsIConverterOutputStream);
converterStream.init(fileStream, charset, string.length,
Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
converterStream.writeString(string);
converterStream.close();
fileStream.close();

Character encoding conversion

Firefox's internal representation of all text is in Unicode. However, in some cases, you may want to be able to process text using another encoding. In these cases, you can easily convert between encodings using the nsIScriptableUnicodeConverter.

Converting from Unicode to other encodings

Listing 17 shows how to convert from text saved as Unicode to EUC-JP encoding.

Listing 17: Converting text from Unicode to EUC-JP

var converter = Components.classes['@mozilla.org/intl/scriptableunicodeconverter']
                .getService(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = 'EUC-JP';
var unicode_str = '\u5909\u63db\u30c6\u30b9\u30c8';
var eucjp_str = converter.ConvertFromUnicode(unicode_str);

Converting from other encodings to Unicode

Listing 18 shows how to do the reverse, converting from text saved as ISO-2022-JP to Unicode.

Listing 18: Converting text from ISO-2022-JP to Unicode

converter.charset = 'ISO-2022-JP';
var unicode_str = converter.ConvertToUnicode(iso2022jp_str);

Reading and writing preferences

You can use the nsIPrefBranch function to access Firefox's preferences system. This function can get and set preferences with three types of values: Boolean, integer, and text; there are specific methods for each, as shown in Table 2.

Reading preferences

Listing 19 shows how to get a text string stored in the preferences. Type about:config into the location bar to confirm that the value has been stored correctly.

Note: The value returned by nsIPrefBranch.getCharPref() is a UTF-8 bytestring; here, we are converting it to UTF-16 using escape() and decodeURIComponent().

Listing 19: Reading a text-string setting

var pref = Components.classes['@mozilla.org/preferences-service;1']
           .getService(Components.interfaces.nsIPrefBranch);
var dir = pref.getCharPref('browser.download.lastDir');
alert(decodeURIComponent(escape(dir)));

Writing preferences

Listing 20 shows the opposite operation, writing a text string to a unique preference. Again, you can use about:config to check that the value has been stored correctly.

Note: The value for the second parameter of nsIPrefBranch.setCharPref() is a UTF-8 bytestring; here, we are converting a UTF-16 to UTF-8 using unescape() and encodeURIComponent().

Listing 20: Writing a text-string setting

var string = 'This is test.';
pref.setCharPref('extensions.myextension.testPref', unescape(encodeURIComponent(string)));
Data type Get Set
Boolean getBoolPref(prefname) setBoolPref(prefname)
Integer getIntPref(prefname) setIntPref(prefname)
Text string getCharPref(prefname) setCharPref(prefname)

Using methods from XUL elements

XPCOM gives you access to sophisticated functions in XUL elements. For example, using the loadURI() method on the browser element introduced in Chapter 3 can open a page specified using HTTP_REFERER, as shown in Listing 21; Listing 22 shows how to open a page using the loadURIWithFlags() method, with data transmitted via the POST method.

Listing 21: Loading a page by setting a referrer

var browser = document.getElementById('browser');
var ioService = Components.classes['@mozilla.org/network/io-service;1']
                .getService(Components.interfaces.nsIIOService);
var referrer = ioService.newURI('http://www.gihyo.co.jp/', null, null);
browser.loadURI('http://www.gihyo.co.jp/magazines/SD', referrer);

Listing 22: Loading a page with data transmitted via the POST method

var content = encodeURIComponent('password=foobar');
var referrer = null;
var postData = Components.classes['@mozilla.org/io/string-input-stream;1']
               .createInstance(Components.interfaces.nsIStringInputStream);
content = 'Content-Type: application/x-www-form-urlencoded\n'+
          'Content-Length: '+content.length+'\n\n'+
          content;
postData.setData(content, content.length);
var flags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
browser.loadURIWithFlags('http://piro.sakura.ne.jp/', flags, referrer, null, postData);