株式会社クリアコード/下田 洋志 SHIMODA Hiroshi
http://www.clear-code.com/

本章では,JavaScriptだけでは行えない高度な処理を実現するため,XPCOMの活用方法を解説します.

はじめに

 JavaScriptには,ファイルの読み書きや文字コードの変換などの処理をする機能はありません.こういった高度な処理を行うには,それ以外の技術を併用する必要があります.Internet ExplorerではそのためのしくみとしてActiveX がありますが,Firefox ではXPCOM(Cross Platform Component Object Model)というしくみを使います.

XPCOMとは

 XPCOMは,プラットフォームに依存しないコンポーネント(プログラムの部品)を開発するためのフレームワークです.また,そのフレームワークに則って開発されたコンポーネントをXPCOMコンポーネントと言い,単に「XPCOM」と言った場合はXPCOMコンポーネントのことを指す場合が多いです.

Firefox はそれ自体が大量のXPCOMコンポーネントを含んでおり,これらは拡張機能からも利用できます.また,必要に応じて独自のXPCOMコンポーネントを開発して拡張機能に同梱することもできます(▽注1).

参考資料

組み込みのXPCOMでどんな機能が使えるかは,MDCのAPIリファレンス(▽注2)や,実際のソースコードのXPIDL(▽注3)によるインターフェース定義などで調べると良いでしょう.Firefox2のソースコードは,Mozilla Cross-Reference(▽注4)で文字列やファイル名などをキーに全文検索することができます.インターフェースの具体的な使い方がわからないときは,実際のソースコードを検索してみるとFirefox内部での使用例を見られるので,参考にしてみると良いでしょう.

XPConnectによるXPCOMの呼び出し

JavaScriptからXPCOMを利用するには,XPConnectという技術を使います.リスト1は,XPConnectを使ってXPCOMのサービスへの参照を取得したり,XPCOMのオブジェクトを新たに生成したりする場合の例です.

各コンポーネントは「@ドメイン名/モジュール名/コンポーネント名;バージョン番号」といった形式のコントラクトIDで識別され,それぞれのコンポーネントが提供するサービスやオブジェクトには「nsI○○」といったインターフェース名が付いています.XPCOMの機能はこのインターフェース名で呼ばれることが多いです.

リスト1:XPConnectを使ってXPCOMの機能を呼び出す例
<?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>

ローカルファイルでXPConnectを使う

リスト1の内容を「test.xul」という名前で保存し,デスクトップ上など適当な所に保存して,ドラッグ&ドロップでFirefoxに読み込ませてください.alert()メソッドが記述されているのに何も起こらないことが確認できます.これは,今作成したtest.xulが特権を持っていないために起こる現象です.

XPConnectを使うには,そのファイルがUniversalXPConnect特権という特別な権限を持っている必要があります.通常のWebページやローカルのファイルは特権を持っていないため,この章で紹介するサンプルコードを実際に試すことはできません.

UniversalXPConnect特権を得るには,リスト2のコードを実行する必要があります.

リスト2:特権を取得するためのコード
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');

ダイアログによる許可

先ほどのtest.xulの「var ioService = ~」という行の前にリスト2の行を加えて,もう一度Firefoxに読み込ませてみてください.今度は図1のような確認のダイアログが表示されます.「許可」ボタンを押すと,この実行コンテキストに UniversalXPConnect特権が与えられ,一時的にXPConnectを利用できるようになります.そのため,今度は「[xpconnect wrapped nsIIOService]」というメッセージが表示されます.

この時,「今後も同様に処理する」にチェックを入れると,それ以後すべてのローカルファイルで確認なしにXPConnectを利用できるようになってしまい大変危険なので,このチェックボックスには絶対にチェックを入れないでください.

図1:特権を与えるかどうかの確認
[図1]

prefs.jsを編集

test.xulを読み込むと,その度に図1の確認ダイアログが表示されます.これが鬱陶しい場合は,ユーザプロファイルフォルダ(▽注5)にあるprefs.jsにリスト3のような記述を追加すると,指定したファイルのスクリプトにおいて,確認なしに特権を取得できるようになります.test.xulをFirefoxに読み込ませている場合,ロケーションバーにファイルURLが表示されます.これをリスト3の「<ファイルのURL>」の部分にコピー&ペーストして,prefs.jsに書き加えましょう.

ただしこの場合も,安全のために,テストが終了した後はprefs.jsからこれらの行を必ず削除しておきましょう.

リスト3:特定のファイルに対して特権取得の確認を表示しない設定
user_pref("capability.principal.codebase.test.granted","UniversalXPConnect");
user_pref("capability.principal.codebase.test.id","<ファイルのURL>");

なお,Firefox自身のコードや拡張機能など,インストールされて登録されたリソースは最初からこの特権を持っています.拡張機能のコードではリスト2のような特権取得のための特別なコードは不要です.

よく使うXPCOMの機能

XPCOMの機能の中でもとくに利用頻度の高いものをいくつか紹介しましょう.サンプルコードを見ながら,XPCOMの利用法のコツを掴んでください.なお,本章のサンプルコードのほとんどは,先ほど作成したtest.xulの中に書いてみることで実際に動作を試すことができます(▽注6).省略個所を適宜補いながら試してみてください.ファイルのパス表現などは,必要に応じて実行環境に合わせた記述に読み替えてください.

ウィンドウの取得

 JavaScriptではそのウィンドウから開いた子ウィンドウを取得することはできますが,まったく関係のないウィンドウやダイアログを取得することはできません.こういった制限を超えてFirefoxのあらゆるウィンドウへのアクセスを可能にする機能が,nsIWindowMediatorです.

アクティブなウィンドウを取得

nsIWindowMediatorでよく使われるのが,アクティブなウィンドウを取得する機能です.リスト4はFirefoxのアクティブなブラウザウィンドウを取得し,開いているタブの数を表示する例です.

リスト4:アクティブなブラウザウィンドウの取得
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);

getMostRecentWindowメソッドの引数としてウィンドウの型名を渡すと,ルート要素のwindowtype属性にその型が指定されているウィンドウの中で,最近アクティブだったものが返されます.

なお,引数にnullを指定すると,ダイアログなども含めてあらゆるウィンドウの中でアクティブなものが返されます.

特定の型のウィンドウ一覧を取得

特定の型のウィンドウの一覧を取得したい場合は,getEnumeratorメソッドを使います.リスト5はFirefoxのすべてのブラウザウィンドウをまとめて閉じる例です.

リスト5:すべてのブラウザウィンドウを閉じる
var browsers = WindowMediator.getEnumerator('navigator:browser');
var browser;
while (browsers.hasMoreElements()) {
    browser = browsers.getNext()
            .QueryInterface(Components.interfaces.nsIDOMWindowInternal);
    browser.BrowserTryToCloseWindow();
}

このメソッドを使うと,指定した型のウィンドウの一覧が,nsISimpleEnumeratorというIteratorパターンのオブジェクトとして返されます.getNextメソッドで要素を取得した後,QueryInterfaceメソッドでインターフェースを取得することで,それぞれの要素をウィンドウオブジェクトとして扱えるようになります.

getEnumeratorメソッドもgetMostRecentWindowメソッドと同様に,引数としてnullを指定すると,ダイアログなども含めてFirefoxのすべてのウィンドウの一覧を取得することができます.

XPCOMによるファイル操作

XPCOMには,WindowsやMac OS X,Linuxといったプラットフォームの違いを気にせずファイル操作を行うためのインターフェースがいくつか用意されています.

ローカルにあるファイルを操作するには,まずリスト6のようにして,ローカルファイルを表すnsILocalFileのオブジェクトを作成します.initWithPathメソッドにファイルやフォルダのフルパス(▽注7)を渡して初期化すると,各種の機能が使えるようになります.このとき,パスは実際に存在するファイルのパスでなくともかまいません.

リスト6:ファイルを表すXPCOMのオブジェクトの作成
var file = Components.classes['@mozilla.org/file/local;1']
    .createInstance(Components.interfaces.nsILocalFile);
file.initWithPath('C:¥¥temp¥¥temp.txt');

ファイルの作成と削除

リスト7は,ファイルが存在する場合は削除して,同じ名前で再度ファイルを生成する例です.

removeメソッドの引数にtrueを渡すと,ファイルやフォルダを再帰的に削除します.フォルダを中身ごと削除する場合はtrueを渡しましょう.

リスト7:ファイルの存在確認,削除,生成の例
file.initWithPath('C:¥¥temp¥¥temp.txt');
if (file.exists()) file.remove(false);
file.create(file.NORMAL_FILE_TYPE, 0666);

createメソッドの第1引数には,生成するファイルの種類を指定します.これらは定数プロパティとして定義されており,通常のファイルの場合はNORMAL_FILE_TYPE,フォルダの場合はDIRECTORY_TYPEを指定します.第2引数は生成するファイルのアクセス権で,UNIX 形式の8進数で指定します(▽注8).

nsILocalFileのオブジェクトは,表1のような,現在のファイルの状態を真偽値で返すメソッドを持っています.

表1:ファイルの状態を調べるメソッド
メソッド機能
exists()ファイルが存在するかどうか
isWriteable()書き込み可能かどうか
isReadable()読み込み可能かどうか
isExecutable()実行可能かどうか
isHidden()隠しファイルかどうか
isFolder()フォルダかどうか
isFile()普通のファイルかどうか
isSymlink()シンボリックリンクかどうか

フォルダの辿り方

フォルダの中に移動する

1つ下位のフォルダ(またはファイル)に移動するには,appendメソッドを使います(リスト8).

リスト8:フォルダを辿る例
file.initWithPath('C:¥¥');
file.append('Documents and Settings');
file.append('All Users');
file.append('Documents');
指定したフォルダ内のファイルを列挙する

フォルダに含まれるすべてのファイルやフォルダを操作したい場合,directoryEntriesプロパティを使います.このプロパティはウィンドウ一覧と同じくnsISimpleEnumerator型のオブジェクトとして内容の一覧を返すので,ウィンドウの場合と同様にしてそれぞれの要素を取得します.リスト9は,フォルダの内容をすべて列挙する例です.

リスト9:特定のフォルダの中身を列挙する例
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'));
親フォルダを取得する

なお,nsILocalFileのオブジェクトには上位のフォルダへ移動する機能はありませんが,リスト10のように,parentプロパティによって現在のファイルの1つ上位のフォルダ(親フォルダ)を取得することはできます.

リスト10:あるファイルのバックアップを別のフォルダに作成する例
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');

ファイルパスとファイルURLの相互変換

XPCOMの機能の中には,リモートのリソースとローカルのファイルの両方に対して使えるものもあり,そういった機能では,対象をURI形式で指定する場合がほとんどです.ローカルのファイルのパスを「file:///C:/temp/temp.txt」といったようなURI(ファイルURL)に変換するには,リスト11のようにします.リスト12はその逆の変換を行う例です.

リスト11:ローカルのファイルパスを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"
リスト12:URLをローカルのファイルパスに変換する例
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"

バイナリファイルの入出力

XPCOMでファイルの入出力を行うには,Javaのようにストリームを使用します.

バイナリファイルの内容を読み込む

リスト13は,ファイルの内容をバイト列(1バイト=8ビットの配列)として取得する例です.試しにASCII文字で「XUL」とだけ書いたテキストファイルを作成して保存し,そのファイルパスを指定してみましょう(▽注9).実行すると「58 55 4C」と表示され,ファイルの内容が読み込まれたことを確認できます.

nsIFileInputStreamの初期化の際は,第2引数と第3引数で「読み込み専用」のモードで初期化しています.処理が終わった後は,必ずすべてのストリームをクローズしてください.

リスト13:バイナリファイルの内容を読み込む例
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());
バイナリファイルを出力する

リスト14はその逆に,バイト列をバイナリファイルとして出力する例です.ここではASCII文字で「XUL」と書かれたテキストファイルを出力しています.

nsIFileOutputStreamの初期化では,第2引数と第3引数で「書き込み専用」のモードで初期化しています.こちらも,処理が終わった後は必ずすべてのストリームをクローズしておきましょう.

テキストファイルの入出力

テキストファイルの読み込みも同様にストリームを使って行います.ただ,ASCII文字以外に日本語などの多バイト文字が含まれる場合は,文字コードの変換が必要です.

テキストファイルの入力

リスト15は,Shift_JISでエンコーディングされたテキストファイルの内容を読み込む例です.試しに,日本語の文章を記述したテキストファイルをShift_JISで保存して,そのファイルパスを指定してみましょう.入力した文章がそのまま表示されます.なお,このようにして読み込んだテキストは,内部ではUnicode(UTF-16)として保持されます.

リスト15:Shift_JISのテキストファイルを読み込む例
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);
テキストファイルの出力

リスト16はその逆に,内部でUnicodeとして保持されている文字列をEUC-JPでエンコーディングしてテキストファイルに出力する例です.ここでは,「変換テスト」という文字列をUnicodeエスケープを用いてJavaScriptのソース中に直接記述したものを出力しています.出力されたファイルを開いて,内容を確認してみましょう.

なお,入出力どちらの場合も,文字コードとしてnullを指定した場合は,UTF-8を指定したのと同じように処理されます.

リスト16: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();

文字コードの変換

Firefoxでは内部においてはすべての文字をUnicodeで処理します.しかし,場合によっては他の文字コードとして文字列を処理したい場合もあるでしょう.このような場合は,nsIScriptableUnicodeConverterインターフェースを使うとテキストの文字コードを自由に変換することができます.

Unicodeから他のエンコーディングへ変換

リスト17は,Unicode形式で保持しているテキストをEUC-JPでエンコーディングされた状態に変換する例です.

リスト17:Unicodeから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);
他のエンコーディングからUnicodeへ変換

リスト18はその逆に,ISO-2022-JPでエンコーディングされたテキストをUnicodeに変換する例です.

リスト18:ISO-2022-JPからUnicodeへ変換する例
converter.charset = 'ISO-2022-JP';
var unicode_str = converter.ConvertToUnicode(iso2022jp_str);

設定の読み書き

nsIPrefBranchの機能を使うと,Firefoxの設定システムにアクセスすることができます.この機能では真偽値,整数値,文字列値の3種類の形式で設定を保存・取得することができ,表2のように,それぞれの形式に対応したメソッドがあります.

表2:設定の形式と対応するメソッド
設定の形式取得保存
真偽値getBoolPref(設定名)setBoolPref(設定名, 真偽値)
整数値getIntPref(設定名)setIntPref(設定名, 整数値)
文字列値getCharPref(設定名)setCharPref(設定名, 文字列値)
設定の読み込み

リスト19は,設定に保存されている文字列値を取得する例です(▽注10).ロケーションバーに「about:config」と入力して,確かにその値が保存されていることを確認してみましょう.

リスト19:文字列値の取得
var pref = Components
        .classes['@mozilla.org/preferences-service;1']
        .getService(Components.interfaces.nsIPrefBranch);
var dir = pref.getCharPref('browser.download.lastDir');
alert(decodeURIComponent(escape(dir)));
設定の書き込み

リスト20はその逆に,独自の設定を文字列として保存する例です(▽注11).こちらも,値が正しく保存されているかどうか,about:configから確認することができます.

リスト20:文字列値の保存
var string = 'This is test.';
pref.setCharPref('extensions.myextension.testPref',
        unescape(encodeURIComponent(string)));

XUL要素のメソッドでの利用

XPCOMを使うと,XUL要素の高度な機能を活用することができます.たとえば第3章で紹介したbrowser要素では,loadURIメソッドを使うことで,リスト21のようにHTTP_REFERERを指定してページを読み込んだり,リスト22のようにloadURIWithFlagsメソッドを使ってPOSTメソッドの送信データ付きでページを読み込んだりすることもできます.

リスト21:リファラを指定してページを読み込む
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);
リスト22:POSTメソッドの送信データ付きでページを読み込む
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);
△注1C++などの言語でコンポーネントを開発する場合は,各プラットフォーム用のバイナリを用意する必要があります.
△注2http://developer.mozilla.org/ja/docs/XPCOM_API_Referencehttps://developer.mozilla.org/ja/XPCOM_API_Reference
△注3IDL(Interface Definition Language)は,オブジェクトのプロパティやメソッドなどの仕様を定義するための言語です.Mozillaで使われるXPIDL(Cross Platform IDL)は,CORBAのIDLを一部拡張したものです.
△注4Firefox 2 の場合は http://mxr.mozilla.org/mozilla1.8/、Firefox 3 の場合は http://mxr.mozilla.org/mozilla/
△注5ユーザプロファイルの位置は環境によって異なります.Windows XPの場合は「C:¥Documents and Settings¥<ユーザ名>¥ApplicationData¥Mozilla¥Firefox¥Profiles¥<ランダムな文字列>.default¥」,Linuxの場合は「~/.mozilla/firefox/<ランダムな文字列>.default/」,Mac OS Xの場合は「~/Library/Application Support/Firefox/Profiles/<ランダムな文字列>.default/」です.
△注6本章で利用しているソースコードは,http://piro.sakura.ne.jp/xul/doc/200704softwaredesign/samples.zip からダウンロードできます.
△注7そのプラットフォームでのパス表現で表記します.Windowsの場合,パス区切りの「¥」記号はエスケープ文字として解釈されるので,必ず2つ書いてください.また,Linux環境などでの「. /」のような特殊な表記は使えません.
△注8Windowsなど,環境によっては指定は無視されます.
△注9ここでは,Cドライブのtempフォルダの中にtemp.txtというファイル名で保存したと仮定しています.
△注9?:サンプルを実際に表示してみる場合,ボタンに表示する画像は,任意の物を用意してXUL文書と同じフォルダに置いてください.
△注10?:Firefox 2では,LinuxのGNOME環境のみ対応しています.
△注10getCharPref()の返り値はUTF-8バイト列なので,ここではescape()とdecodeURIComponent()を使ってUTF-16に変換しています.
△注11setCharPref()の第2引数の形式はUTF-8バイト列なので,ここではUTF-16文字列をunescape()とencodeURIComponent()を使ってUTF-8バイト列に変換しています.