本章では,JavaScriptだけでは行えない高度な処理を実現するため,XPCOMの活用方法を解説します.
JavaScriptには,ファイルの読み書きや文字コードの変換などの処理をする機能はありません.こういった高度な処理を行うには,それ以外の技術を併用する必要があります.Internet ExplorerではそのためのしくみとしてActiveX がありますが,Firefox ではXPCOM(Cross Platform Component Object Model)というしくみを使います.
XPCOMは,プラットフォームに依存しないコンポーネント(プログラムの部品)を開発するためのフレームワークです.また,そのフレームワークに則って開発されたコンポーネントをXPCOMコンポーネントと言い,単に「XPCOM」と言った場合はXPCOMコンポーネントのことを指す場合が多いです.
Firefox はそれ自体が大量のXPCOMコンポーネントを含んでおり,これらは拡張機能からも利用できます.また,必要に応じて独自のXPCOMコンポーネントを開発して拡張機能に同梱することもできます(▽注1).
組み込みのXPCOMでどんな機能が使えるかは,MDCのAPIリファレンス(▽注2)や,実際のソースコードのXPIDL(▽注3)によるインターフェース定義などで調べると良いでしょう.Firefox2のソースコードは,Mozilla Cross-Reference(▽注4)で文字列やファイル名などをキーに全文検索することができます.インターフェースの具体的な使い方がわからないときは,実際のソースコードを検索してみるとFirefox内部での使用例を見られるので,参考にしてみると良いでしょう.
JavaScriptからXPCOMを利用するには,XPConnectという技術を使います.リスト1は,XPConnectを使ってXPCOMのサービスへの参照を取得したり,XPCOMのオブジェクトを新たに生成したりする場合の例です.
各コンポーネントは「@ドメイン名/モジュール名/コンポーネント名;バージョン番号」といった形式のコントラクトIDで識別され,それぞれのコンポーネントが提供するサービスやオブジェクトには「nsI○○」といったインターフェース名が付いています.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>
リスト1の内容を「test.xul」という名前で保存し,デスクトップ上など適当な所に保存して,ドラッグ&ドロップでFirefoxに読み込ませてください.alert()メソッドが記述されているのに何も起こらないことが確認できます.これは,今作成したtest.xulが特権を持っていないために起こる現象です.
XPConnectを使うには,そのファイルがUniversalXPConnect特権という特別な権限を持っている必要があります.通常のWebページやローカルのファイルは特権を持っていないため,この章で紹介するサンプルコードを実際に試すことはできません.
UniversalXPConnect特権を得るには,リスト2のコードを実行する必要があります.
netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
先ほどのtest.xulの「var ioService = ~」という行の前にリスト2の行を加えて,もう一度Firefoxに読み込ませてみてください.今度は図1のような確認のダイアログが表示されます.「許可」ボタンを押すと,この実行コンテキストに UniversalXPConnect特権が与えられ,一時的にXPConnectを利用できるようになります.そのため,今度は「[xpconnect wrapped nsIIOService]」というメッセージが表示されます.
この時,「今後も同様に処理する」にチェックを入れると,それ以後すべてのローカルファイルで確認なしにXPConnectを利用できるようになってしまい大変危険なので,このチェックボックスには絶対にチェックを入れないでください.
test.xulを読み込むと,その度に図1の確認ダイアログが表示されます.これが鬱陶しい場合は,ユーザプロファイルフォルダ(▽注5)にあるprefs.jsにリスト3のような記述を追加すると,指定したファイルのスクリプトにおいて,確認なしに特権を取得できるようになります.test.xulをFirefoxに読み込ませている場合,ロケーションバーにファイルURLが表示されます.これをリスト3の「<ファイルのURL>」の部分にコピー&ペーストして,prefs.jsに書き加えましょう.
ただしこの場合も,安全のために,テストが終了した後はprefs.jsからこれらの行を必ず削除しておきましょう.
user_pref("capability.principal.codebase.test.granted","UniversalXPConnect"); user_pref("capability.principal.codebase.test.id","<ファイルのURL>");
なお,Firefox自身のコードや拡張機能など,インストールされて登録されたリソースは最初からこの特権を持っています.拡張機能のコードではリスト2のような特権取得のための特別なコードは不要です.
XPCOMの機能の中でもとくに利用頻度の高いものをいくつか紹介しましょう.サンプルコードを見ながら,XPCOMの利用法のコツを掴んでください.なお,本章のサンプルコードのほとんどは,先ほど作成したtest.xulの中に書いてみることで実際に動作を試すことができます(▽注6).省略個所を適宜補いながら試してみてください.ファイルのパス表現などは,必要に応じて実行環境に合わせた記述に読み替えてください.
JavaScriptではそのウィンドウから開いた子ウィンドウを取得することはできますが,まったく関係のないウィンドウやダイアログを取得することはできません.こういった制限を超えてFirefoxのあらゆるウィンドウへのアクセスを可能にする機能が,nsIWindowMediatorです.
nsIWindowMediatorでよく使われるのが,アクティブなウィンドウを取得する機能です.リスト4はFirefoxのアクティブなブラウザウィンドウを取得し,開いているタブの数を表示する例です.
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のすべてのブラウザウィンドウをまとめて閉じる例です.
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には,WindowsやMac OS X,Linuxといったプラットフォームの違いを気にせずファイル操作を行うためのインターフェースがいくつか用意されています.
ローカルにあるファイルを操作するには,まずリスト6のようにして,ローカルファイルを表すnsILocalFileのオブジェクトを作成します.initWithPathメソッドにファイルやフォルダのフルパス(▽注7)を渡して初期化すると,各種の機能が使えるようになります.このとき,パスは実際に存在するファイルのパスでなくともかまいません.
var file = Components.classes['@mozilla.org/file/local;1'] .createInstance(Components.interfaces.nsILocalFile); file.initWithPath('C:¥¥temp¥¥temp.txt');
リスト7は,ファイルが存在する場合は削除して,同じ名前で再度ファイルを生成する例です.
removeメソッドの引数にtrueを渡すと,ファイルやフォルダを再帰的に削除します.フォルダを中身ごと削除する場合はtrueを渡しましょう.
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のような,現在のファイルの状態を真偽値で返すメソッドを持っています.
メソッド | 機能 |
---|---|
exists() | ファイルが存在するかどうか |
isWriteable() | 書き込み可能かどうか |
isReadable() | 読み込み可能かどうか |
isExecutable() | 実行可能かどうか |
isHidden() | 隠しファイルかどうか |
isFolder() | フォルダかどうか |
isFile() | 普通のファイルかどうか |
isSymlink() | シンボリックリンクかどうか |
1つ下位のフォルダ(またはファイル)に移動するには,appendメソッドを使います(リスト8).
file.initWithPath('C:¥¥'); file.append('Documents and Settings'); file.append('All Users'); file.append('Documents');
フォルダに含まれるすべてのファイルやフォルダを操作したい場合,directoryEntriesプロパティを使います.このプロパティはウィンドウ一覧と同じくnsISimpleEnumerator型のオブジェクトとして内容の一覧を返すので,ウィンドウの場合と同様にしてそれぞれの要素を取得します.リスト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つ上位のフォルダ(親フォルダ)を取得することはできます.
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');
XPCOMの機能の中には,リモートのリソースとローカルのファイルの両方に対して使えるものもあり,そういった機能では,対象をURI形式で指定する場合がほとんどです.ローカルのファイルのパスを「file:///C:/temp/temp.txt」といったようなURI(ファイルURL)に変換するには,リスト11のようにします.リスト12はその逆の変換を行う例です.
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"
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引数で「読み込み専用」のモードで初期化しています.処理が終わった後は,必ずすべてのストリームをクローズしてください.
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)として保持されます.
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を指定したのと同じように処理されます.
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インターフェースを使うとテキストの文字コードを自由に変換することができます.
リスト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);
リスト18はその逆に,ISO-2022-JPでエンコーディングされたテキストをUnicodeに変換する例です.
converter.charset = 'ISO-2022-JP'; var unicode_str = converter.ConvertToUnicode(iso2022jp_str);
nsIPrefBranchの機能を使うと,Firefoxの設定システムにアクセスすることができます.この機能では真偽値,整数値,文字列値の3種類の形式で設定を保存・取得することができ,表2のように,それぞれの形式に対応したメソッドがあります.
設定の形式 | 取得 | 保存 |
---|---|---|
真偽値 | getBoolPref(設定名) | setBoolPref(設定名, 真偽値) |
整数値 | getIntPref(設定名) | setIntPref(設定名, 整数値) |
文字列値 | getCharPref(設定名) | setCharPref(設定名, 文字列値) |
リスト19は,設定に保存されている文字列値を取得する例です(▽注10).ロケーションバーに「about:config」と入力して,確かにその値が保存されていることを確認してみましょう.
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から確認することができます.
var string = 'This is test.'; pref.setCharPref('extensions.myextension.testPref', unescape(encodeURIComponent(string)));
XPCOMを使うと,XUL要素の高度な機能を活用することができます.たとえば第3章で紹介したbrowser要素では,loadURIメソッドを使うことで,リスト21のようにHTTP_REFERERを指定してページを読み込んだり,リスト22のようにloadURIWithFlagsメソッドを使ってPOSTメソッドの送信データ付きでページを読み込んだりすることもできます.
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);
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);
△注1: | C++などの言語でコンポーネントを開発する場合は,各プラットフォーム用のバイナリを用意する必要があります. |
△注2: | |
△注3: | IDL(Interface Definition Language)は,オブジェクトのプロパティやメソッドなどの仕様を定義するための言語です.Mozillaで使われるXPIDL(Cross Platform IDL)は,CORBAのIDLを一部拡張したものです. |
△注4: | Firefox 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環境などでの「. /」のような特殊な表記は使えません. |
△注8: | Windowsなど,環境によっては指定は無視されます. |
△注9: | ここでは,Cドライブのtempフォルダの中にtemp.txtというファイル名で保存したと仮定しています. |
△注9?: | サンプルを実際に表示してみる場合,ボタンに表示する画像は,任意の物を用意してXUL文書と同じフォルダに置いてください. |
△注10?: | Firefox 2では,LinuxのGNOME環境のみ対応しています. |
△注10: | getCharPref()の返り値はUTF-8バイト列なので,ここではescape()とdecodeURIComponent()を使ってUTF-16に変換しています. |
△注11: | setCharPref()の第2引数の形式はUTF-8バイト列なので,ここではUTF-16文字列をunescape()とencodeURIComponent()を使ってUTF-8バイト列に変換しています. |