Now browsing the archives for the 'XPCOM' category.

nsISafeOutputStream で安全なファイルの書き込み

(1) 通常のファイルの書き込み

function writeFile(aFile, aText) {
    var stream = Cc["@mozilla.org/network/file-output-stream;1"].
                 createInstance(Ci.nsIFileOutputStream);
    stream.init(aFile, 0x02 | 0x08 | 0x20, 0644, 0);
    stream.write(aText, aText.length);
    stream.close();
}

(2) nsISafeOutputStream を使った安全なファイルの書き込み

function writeFileSafely(aFile, aText) {
    var stream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
                 createInstance(Ci.nsIFileOutputStream);
    stream.init(aFile, 0x02 | 0x08 | 0x20, 0644, 0);
    stream.write(aText, aText.length);
    stream.QueryInterface(Ci.nsISafeOutputStream);
    stream.finish();
}

(1) の方式は多分ファイル書き込み時にいったん0バイトにしてから書き込みが行われるため、ファイルの書き込み中の強制終了などにより破損する可能性が高い。
一方、(2) の方式は “test-1.txt” のような別名でいったんファイルの書き出しが行われた後、その内容を本来のファイル “test.txt” へ上書きコピーするため、破損の可能性が低いと思われる。
パフォーマンスの面では、簡単なベンチマークを試したところ (2) が劣るようであったが、書き込むデータサイズが大きい(数MB以上)場合はほとんど差異がなくなるようだった。

TOP

JavaScript 関数と XPCOM メソッドの例外ハンドリング

JavaScript の関数がスローする例外の内容を知るには、例外オブジェクトの値そのものを調べる。
XPCOM のメソッドがスローする例外の内容を知るには、例外オブジェクト (nsIXPCException オブジェクト) の result プロパティなどを調べる。

例えば以下のような純粋な JavaScript の関数があるとすると、

const Cr = Components.results;
function test() {
    throw Cr.NS_ERROR_FAILURE;
}

関数実行時に catch したオブジェクトの値そのものを調べることで例外の内容を知ることができる。

try {
    test();
}
catch (ex if ex == Cr.NS_ERROR_FAILURE) {
    alert("Failed!");
}

一方、上記の関数 test と同じ内容のメソッドを JavaScript 製 XPCOM のメソッドとして実装した場合、メソッド実行時に catch した例外は nsIXPCException オブジェクトとなり、 result プロパティなどから例外の内容を知ることができる。

try {
    Cc["@XXX"].getService(Ci.XXX).test();
}
catch (ex if ex.result == Cr.NS_ERROR_FAILURE) {
    alert("Failed!");
}

Components.Exception コンストラクタを使えば、純粋な JavaScript の関数で nsIXPCException オブジェクトの例外をスローすることも可能。エラーコンソールに例外発生のソースファイルなどの詳細表示ができるといった利点が挙げられる。

function test() {
    throw new Components.Exception("Failed!", Cr.NS_ERROR_FAILURE);
}

TOP

nsIWebBrowserPersist の基本的な使い方 (5) ~ ダウンロード進捗状況

nsIWebBrowserPersist で HTTP によってダウンロードする際、ダウンロードの進捗状況を監視する。
サンプルコードは nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形 をベースとしており、一部省略しています。

saveURI の場合

nsIWebBrowserPersist の progressListener プロパティに nsIWebProgressListener インタフェースを実装したオブジェクトを自前で作成してセットすると、ダウンロードの状況の変化によって以下のメソッドが呼び出される。

メソッド名 呼び出されるタイミング
onStateChange 引数 aStateFlags の値を調べ、 nsIWebProgressListener.STATE_START フラグが立っている場合はダウンロード開始、 nsIWebProgressListener.STATE_STOP フラグが立っている場合はダウンロード終了。
onProgressChange ダウンロード進行中に呼び出される。引数 aCurSelfProgress, aMaxSelfProgress の値を調べることで、合計何バイト中の何バイトをダウンロードしたかがわかる。
onLocationChange 呼び出し無し。(xul:tabbrowser 要素の持つ nsIWebProgress オブジェクト専用?)
onStatusChange
onSecurityChange
wbp.progressListener = {
    // implements nsIWebProgressListener
    onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
        if (aStateFlags & Ci.nsIWebProgressListener.STATE_START)
            dump("started
");
        if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
            dump("stopped
");
    },
    onProgressChange: function (aWebProgress, aRequest,
                                aCurSelfProgress, aMaxSelfProgress,
                                aCurTotalProgress, aMaxTotalProgress) {
        dump("downloading... " + aCurSelfProgress + "/" + aMaxSelfProgress + "
");
    },
    onLocationChange: function (aWebProgress, aRequest, aLocation) {},
    onStatusChange  : function (aWebProgress, aRequest, aStatus, aMessage) {},
    onSecurityChange: function (aWebProgress, aRequest, aState) {},
};

saveChannel の場合

saveURI の場合と異なり、なぜか nsIWebProgressListener の onProgressChange メソッドが呼び出されない。代わりに、 nsIChannel の notificationCallbacks プロパティに nsIProgressEventSink インタフェースを実装する。
参考: nsIWebBrowserPersist.saveChannel – やんばるもじら

channel.notificationCallbacks = {
    QueryInterface: function (aIID) {
        if (aIID.equals(Ci.nsIProgressEventSink))
            return this;
        Components.returnCode = Cr.NS_ERROR_NO_INTERFACE;
        return null;
    },
    // implements nsIInterfaceRequestor
    getInterface: function (aIID, aInstance) {
        return this.QueryInterface(aIID);
    },
    // implements nsIProgressEventSink
    onProgress: function (aRequest, aContext, aProgress, aProgressMax) {
        dump("downloading... " + aProgress + "/" + aProgressMax + "
");
    },
    onStatus: function (aRequest, aContext, aStatus, aStatusArg) {},
};

nsIChannel.notificationCallbacks プロパティは nsIInterfaceRequestor 型であり、 nsIChannel オブジェクトに発生する様々なイベントに応じて、まずはじめに getInterface メソッドが呼び出される。 getInterface メソッドは引数にて指定されたインタフェースへと QI して返すだけ。

なお、 nsIChannel.asyncOpen によって要求開始した場合、 notificationCallbacks に nsIRequestObserver インタフェースを実装することで、要求開始時と要求終了時に onStartRequest, onStopRequest メソッドが呼び出されるが、 nsIWebBrowserPersist.saveChannel によるダウンロードではこの呼び出しは発生しない。

別の方法として、 notificationCallbacks プロパティに nsIWebBrowserPersist オブジェクト自体をセットする手もあるようだ。

wbp.progressListener = {
    /* snip */
};

channel.notificationCallbacks = wbp;

参考

nsIWebProgressListener.idl
nsIInterfaceRequestor.idl
nsIProgressEventSink.idl

関連記事

nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形
nsIWebBrowserPersist の基本的な使い方 (2) ~ persistFlags
nsIWebBrowserPersist の基本的な使い方 (3) ~ 各種ヘッダの追加
nsIWebBrowserPersist の基本的な使い方 (4) ~ POST メソッド
nsIWebBrowserPersist の基本的な使い方 (5) ~ ダウンロード進捗状況
つづく…?

TOP

nsIWebBrowserPersist の基本的な使い方 (4) ~ POST メソッド

HTTP の POST メソッドで Web サーバへ要求し、その回答結果をファイルへ保存する。
ほとんど nsIHttpChannel の使用がメインですので、ファイル保存する場合以外にも応用可能です。
また、サンプルコードは nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形 をベースとしており、一部省略しています。

saveURI の場合

saveURI の第4引数 aPostData を使えばできるはずだが、現在調査中…

saveChannel の場合

例によって nsIURL オブジェクトから nsIHttpChannel オブジェクトを生成する。

// make nsIHttpChannel
var ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var channel = ioSvc.newChannelFromURI(url).QueryInterface(Ci.nsIHttpChannel);

POST するデータは以下のような文字列とする。以下の場合は文字列全体を単純に encodeURI 関数で URL エンコードすれば良いが、キーや値に & や = を含む場合を考慮した、より一般的な URL エンコードの方法については後述する。

var postStr = encodeURI("foo=bar&baz=eek&名前=太郎");

POST する文字列から nsIStringInputStream を生成し、 nsIHttpChannel を nsIUploadChannel へ QI してからストリームをセットする。今回の例では Content-type は application/x-www-form-urlencoded だが、 XML データを POST する場合などは適宜 application/xml とかにする。

// make nsIStringInputStream to post
var inputStream = Cc["@mozilla.org/io/string-input-stream;1"]
                  .createInstance(Ci.nsIStringInputStream);
inputStream.setData(postStr, postStr.length);

// set nsIStringInputStream to nsIUploadChannel
var uploadChannel = channel.QueryInterface(Ci.nsIUploadChannel);
uploadChannel.setUploadStream(inputStream, "application/x-www-form-urlencoded", -1);

nsIWebBrowserPersist で送信する前に、 nsIHttpChannel オブジェクトの requestMethod プロパティを POST にするのを忘れずに。これをしないと、なぜか PUT メソッドでの要求になってしまう。

// must do this otherwise request method will be "PUT"
channel.requestMethod = "POST";

// save channel to file
var wbp = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
          .createInstance(Ci.nsIWebBrowserPersist);
wbp.saveChannel(channel, file);

POST する文字列の URL エンコード

POST する文字列がユーザからの入力である場合、 & や = を含む場合を考慮し、以下のように encodeURIComponent を使って URL エンコードする。

// make string to post
var postObj = {
    "foo": "bar",
    "baz": "eek",
    "名前": "山田=太郎&花子",
};
var pairs = [];
for (var [key, val] in Iterator(postObj)) {
    pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(val));
}
var postStr = pairs.join("&");

関連記事

nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形
nsIWebBrowserPersist の基本的な使い方 (2) ~ persistFlags
nsIWebBrowserPersist の基本的な使い方 (3) ~ 各種ヘッダの追加
nsIWebBrowserPersist の基本的な使い方 (4) ~ POST メソッド
nsIWebBrowserPersist の基本的な使い方 (5) ~ ダウンロード進捗状況
つづく…?

TOP

nsIWebBrowserPersist の基本的な使い方 (3) ~ 各種ヘッダの追加

リファラを指定する

事前準備として、リファラの URL 文字列から nsIURL オブジェクトを生成する。

var refURL = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIURL);
refURL.spec = "http://www.itmedia.co.jp/";
saveURI の場合

saveURI メソッドの第3引数 aReferrer に nsIURI オブジェクトを指定することでリファラをセットできる。
Live HTTP Headers で要求時のヘッダを見ると、「Referer: http://www.itmedia.co.jp/」というヘッダが付いている。

wbp.saveURI(url, null, refURL, null, null, file);
saveChannel の場合

nsIHttpChannel の referrer プロパティに nsIURI オブジェクトを指定する。

channel.referrer = refURL;

任意のヘッダを指定する

saveURI の場合

saveURI メソッドの第5引数 aExtraHeaders にヘッダを文字列として指定すると、HTTP要求時にそのヘッダを追加できる。
ヘッダは「Foo: bar
」のような形式で指定する。ただし、既存ヘッダを上書きすることはできず、必ず追加される。
例えば以下のようにすると、「Accept-Encoding: gzip,deflate, xxx」となる。

wbp.saveURI(url, null, null, null, "Accept-Encoding: xxx
", file);
saveChannel の場合

nsIHttpChannel では、 setRequestHeader メソッドで自由にヘッダを追加できる。
第3引数 aMerge を true にすると既存ヘッダを上書きせずに追加し、 false にすると既存ヘッダを上書きする。

channel.setRequestHeader("User-Agent", "HTTP Downloader", false);

以下のようにすると、「Accept-Encoding」ヘッダ自体を送らなくすることができる。

channel.setRequestHeader("Accept-Encoding", null, false);

関連記事

nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形
nsIWebBrowserPersist の基本的な使い方 (2) ~ persistFlags
nsIWebBrowserPersist の基本的な使い方 (3) ~ 各種ヘッダの追加
nsIWebBrowserPersist の基本的な使い方 (4) ~ POST メソッド
nsIWebBrowserPersist の基本的な使い方 (5) ~ ダウンロード進捗状況
つづく…?

TOP

nsIWebBrowserPersist の基本的な使い方 (2) ~ persistFlags

saveURI / saveChannel を実行する前に nsIWebBrowserPersist オブジェクトの persistFlags プロパティに各種フラグをセットすることで、ダウンロードや保存の方式のオプションを指定できる。

PERSIST_FLAGS_BYPASS_CACHE

このフラグを立てると、キャッシュを無視して毎回Webサーバからダウンロードする。
厳密には、要求時に「Pragma: no-cache」「Cache-Control: no-cache」の2つのヘッダが付加される。

wbp.persistFlags |= wbp.PERSIST_FLAGS_BYPASS_CACHE;

PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION

このフラグを立てると、gzip圧縮転送された場合に非圧縮状態でファイルへ保存してくれる。
このフラグを立てないと、gzip圧縮転送されたデータは圧縮されたままファイルへと保存してしまうので注意。
あえて圧縮状態で保存する理由が思いつかないので、基本的にこのフラグは必須と考えてよさそう。

wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;

PERSIST_FLAGS_APPEND_TO_FILE

Firefox 3 以降で対応。このフラグを立てると、ダウンロードしたデータを既存ファイルへ上書きせずに追記する。

wbp.persistFlags |= wbp.PERSIST_FLAGS_APPEND_TO_FILE;

参考

上記以外にも色々なフラグがある。詳しくは nsIWebBrowserPersist.idl を参照。
nsIWebBrowserPersist.idl

関連記事

nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形
nsIWebBrowserPersist の基本的な使い方 (2) ~ persistFlags
nsIWebBrowserPersist の基本的な使い方 (3) ~ 各種ヘッダの追加
nsIWebBrowserPersist の基本的な使い方 (4) ~ POST メソッド
nsIWebBrowserPersist の基本的な使い方 (5) ~ ダウンロード進捗状況
つづく…?

TOP

nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形

指定したURLからダウンロードしてファイルへ保存する際には nsIWebBrowserPersist が便利。
HTTPだけでなく、FTP、file:やchrome:プロトコルなどのURLからのファイル保存も可能ですが、ここではHTTPを前提とします。

nsIWebBrowserPersist の主要メソッド

メソッド 概要
saveURI() 指定したURIからダウンロードしてファイルへ保存する。
saveChannel() 指定したチャネル(nsIHttpChannel など)をファイルへ保存する。
saveDocument() 指定したDOMドキュメントをファイルへ保存する。

基本形

指定したURLのデータをダウンロードし、指定したパスのファイルへと保存するだけの簡単なコードを作る。
まずは URL 文字列から nsIURL オブジェクトを、ファイルパスから nsILocalFile オブジェクトを生成する。

const URL_SPEC = "http://www.mozilla.com/img/firefox-title.png";
const FILE_PATH = "C:firefox-title.png";

// make nsIURL
var url = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIURL);
url.spec = URL_SPEC;

// make nsILocalFile
var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(FILE_PATH);
saveURI の場合

nsIWebBrowserPersist のインスタンスを生成し、先ほどの2つのオブジェクトを引数に saveURI を実行するだけ。

// save URL to file
var wbp = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
          .createInstance(Ci.nsIWebBrowserPersist);
wbp.saveURI(url, null, null, null, null, file);
saveChannel の場合

色々なものを引数で指定できる便利屋的な saveURI メソッドに対して、 saveChannel メソッドは nsIChannel を引数とすることでより詳細な実装が可能。
nsIIOService を使って nsIURL オブジェクトから nsIHttpChannel オブジェクトを生成し、 nsIHttpChannel と nsILocalFile を引数に saveChannel メソッドを実行する。

// make nsIHttpChannel
var ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var channel = ioSvc.newChannelFromURI(url).QueryInterface(Ci.nsIHttpChannel);

// save channel to file
var wbp = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
          .createInstance(Ci.nsIWebBrowserPersist);
wbp.saveChannel(channel, file);

なお、 nsIChannel を saveChannel に渡す前に open / asyncOpen してはならない。

参考

nsIWebBrowserPersist.idl
nsIIOService.idl
nsIChannel.idl
nsIHttpChannel.idl

関連記事

nsIWebBrowserPersist の基本的な使い方 (1) ~ 基本形
nsIWebBrowserPersist の基本的な使い方 (2) ~ persistFlags
nsIWebBrowserPersist の基本的な使い方 (3) ~ 各種ヘッダの追加
nsIWebBrowserPersist の基本的な使い方 (4) ~ POST メソッド
nsIWebBrowserPersist の基本的な使い方 (5) ~ ダウンロード進捗状況
つづく…?

TOP

XULPlanet の XPCOM リファレンスが Firefox 3 / Mozilla 1.9 ベースに更新

結局、XULPlanet はまだ存続するようだ。
特に使用頻度の高い XPCOM のリファレンスが、 Firefox 2 / Mozilla 1.8 ベースから Firefox 3 / Mozilla 1.9 ベースへと更新された。確かに FUEL とか Places の API も反映されている。

XPCOM Reference

このリファレンスは各 XPCOM インタフェースの IDL ファイルから Perl のスクリプトで一括変換して生成しているみたいだが、最近一部のリファレンスに、元の IDL ファイルにコメントで記載されている情報が抜け落ちていることに気付いた。

mozilla mozilla/embedding/components/webbrowserpersist/public/nsIWebBrowserPersist.idl
Interface Reference – nsIWebBrowserPersist

この2つを比べるとわかるが、IDL の方にコメントで記載されている「No special persistence behaviour.」という内容(”PERSIST_FLAGS_NONE” についての説明)は、 XULPlanet のリファレンスには掲載されていない。おそらく「/**」~「*/」という形式のコメントを無視してリファレンスを生成しているのだろうか。とりあえず Neil Deakin 氏のブログにコメントしてみた。

TOP

nsIWebBrowserPersist で gzip 圧縮されたデータをダウンロードする

HTTPでダウンロードする際は nsIWebBrowserPersist::saveURI や saveChannel が便利だ。
しかし、gzip圧縮形式で転送されたデータ(つまりWebサーバが「Content-Encoding: gzip」ヘッダを付加した場合)をダウンロードすると、圧縮されたままの状態でファイルへと保存してしまう。

非圧縮状態でファイルへ保存するためには、 nsIWebBrowserPersist::persistFlags プロパティに PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION フラグを立てる。
あえて圧縮状態で保存する理由が思いつかないので、基本的にこのフラグは必須と考えてよさそう。

参考

nsIWebBrowserPersist.idl

余談

ScrapBook でgzip圧縮されたページを保存すると画像がすべて壊れて表示されなかったり、 Amazon や はてな のページを保存すると、ツリーの favicon が消滅してしまう、といった問題はすべて上記の問題によるものだった。さきほど修正して新しいバージョンをリリースした。

なぜこんな簡単なことに今まで気付かなかったのだろうか…。
しかも気付く前に Bugzilla にバグを立ててしまった。
Bug 416817 – nsIWebBrowserPersist should decompress data if server responses with "Content-Encoding: gzip" header

TOP

タブの複製機能

Firefox 3 (Minefield) で新たに追加されたタブの複製機能(Ctrl キーを押下しながらのタブのドラッグ&ドロップ)が、戻る・進むの履歴などのセッション状態も含めてタブを複製できるようになった。

これに伴い、 nsISessionStore インタフェースに以下の3メソッドが追加された。

メソッド 概要
duplicateTab() 指定したタブを、セッション状態も含めて複製する
getTabState() 指定したタブのセッション状態を JSON 文字列として取得する
setTabState() タブのセッション状態を表す JSON 文字列からタブを復元する

duplicateTab はすでに閉じたタブの状態を復元するのではなく、現在開いているタブの状態を複製する機能なので、 browser.sessionstore.enabled が false でも動作するようだ。

nsISessionStore::duplicateTab 使用例

// an example code to duplicate |tab| in the current window
var newTab;
try {
    newTab = Cc["@mozilla.org/browser/sessionstore;1"]
             .getService(Ci.nsISessionStore)
             .duplicateTab(window, tab);
}
catch (ex) {
    // fall-back
    newTab = gBrowser.loadOneTab(gBrowser.getBrowserForTab(tab).currentURI.spec);
}

xul:tabbrowser 要素自体に定義された duplicateTab メソッドがまさに上記のことをやっているので、これを使う方が手っ取り早い。

var newTab = gBrowser.duplicateTab(tab);

参考

Bug 393716 – Add single tab save/restore to session store API

TOP