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.
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.
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.
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');
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.
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.
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.
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.
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.
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.
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);