Now browsing the archives for the 'XPCOM' category.

【つづき】 JavaScript 製 XPCOM で配列構造・列挙構造のデータをメソッドの戻り値にする

前回のエントリでコメントを頂いていたのに気付くのが遅く、だいぶ日があいてしまったが、配列構造のデータをメソッドの戻り値にするためには nsIArray よりも nsIVariant 型を使うのが手っ取り早そうである。

IDL

nsIVariant getFruitsArray();

XPCOM 実装

getFruitsArray: function() {
    var createStringObject = function(aStr) {
        var obj = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
        obj.data = aStr;
        return obj;
    };
    var items = [
        createStringObject("apple"),
        createStringObject("orange"),
        createStringObject("banana"),
    ];
    return items;
},

利用する側の JavaScript

var svc = Cc["********"].getService(Ci.********);
var arr = svc.getFruitsArray();
arr.forEach(function(elt) {
    alert(elt.data);
});

TOP

JavaScript 製 XPCOM で配列構造・列挙構造のデータをメソッドの戻り値にする

JavaScript 製 XPCOM コンポーネントではメソッドの戻り値に JavaScript の配列 (Array オブジェクト) をそのまま使用することができない *1 。その理由は、 XPCOM のインタフェースは各言語固有の仕様に依存しない仕組みであるからだろう。 なんとか配列構造(もしくは列挙構造)のデータを戻り値にする方法はないものかと Firefox のソースなどを探ったところ、以下のような方法があることがわかった。

nsIArray / nsIMutableArray インタフェースで配列構造のデータを受け渡す

nsIArray は、配列構造のデータから個々の要素を取り出すためのインタフェースである。また、 nsIMutableArray は、個々の要素から配列構造のデータを組み立てるためのインタフェースであり、 nsIArray を継承する。

これら2つのインタフェースを利用すると、 JS 製 XPCOM 側のメソッド内にて戻り値を nsIMutableArray 型オブジェクトにしてやり、 XPCOM を利用して戻り値を受け取る側の JS から nsIArray インタフェースを介して配列データおよび個々の要素を取り出すことが可能となる。

まず、IDL には以下のような感じでメソッドを定義する。

nsIArray getFruitsArray();

次に、JS 製 XPCOM で以下のような感じでメソッドを実装する。 nsIMutableArray#appendElement メソッドによって3つの nsISupportsString 型オブジェクトを nsIMutableArray オブジェクトへ追加している。

getFruitsArray: function() {
    var createStringObject = function(aStr) {
        var obj = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
        obj.data = aStr;
        return obj;
    };
    var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
    items.appendElement(createStringObject("apple"), false);
    items.appendElement(createStringObject("orange"), false);
    items.appendElement(createStringObject("banana"), false);
    return items;
},

すると、以下のような感じで XPCOM を利用する側の JS から配列を受け取って個々の要素を取り出すことができる。 nsIArray#queryElementAt メソッドは JavaScript の Array オブジェクトで言うところの [i] のように、指定したインデックスの要素を取り出すことができる。ただし、 nsIArray の IDL は未凍結であるためか IDL に記載された仕様と実際の仕様が異なることに注意。

var svc = Cc["********"].getService(Ci.********);
var arr = svc.getFruitsArray();
for (var i = 0; i < arr.length; i++) {
    var elt = arr.queryElementAt(i, Ci.nsISupportsString);
    alert(elt.data);
}

別の取り出し方として、 nsIArray#enumerate メソッドを使い、 nsISimpleEnumerator インタフェースを介して列挙構造として個々の要素を取得することも可能。

var enum = arr.enumerate();
while (enum.hasMoreElements()) {
    var elt = enum.getNext().QueryInterface(Ci.nsISupportsString);
    alert(elt.data);
}

nsISimpleEnumerator インタフェースで列挙構造のデータを受け渡す

インデックス付きの配列構造ではなく、単純な列挙構造でよければ、 nsISimpleEnumerator インタフェースを介してデータを受け渡しする手もある。ただ、 nsIMutableArray とは違って列挙構造を組み立てるためのインタフェースというのが存在しないらしく、必要となるメソッドを自前で実装しなければならない。

まず、IDL には以下のような感じでメソッドを定義する。

nsISimpleEnumerator getFruitsEnumerator();

次に、JS 製 XPCOM で以下のような感じでメソッドを実装する。

getFruitsEnumerator: function() {
    var createStringObject = function(aStr) {
        var obj = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
        obj.data = aStr;
        return obj;
    };
    var items = [];
    items.push(createStringObject("apple"));
    items.push(createStringObject("orange"));
    items.push(createStringObject("banana"));
    return new ArrayEnumerator(items);
},

最後の return 時に使用する ArrayEnumerator クラスは、内部的なデータとして JavaScript の配列オブジェクトを保持し、 nsISimpleEnumerator インタフェースが持つふたつのメソッド (hasMoreElements と getNext) を実装することで列挙も可能なクラスであり、 nsMicrosummaryService.js を参考に別途実装する。

XPCOM を利用する側の JS からの取り出し方は前述の nsIArray#enumerate メソッドを使用した手順とほぼ同じ。

var svc = Cc["********"].getService(Ci.********);
var enum = svc.getFruitsEnumerator();
while (enum.hasMoreElements()) {
    var elt = enum.getNext().QueryInterface(Ci.nsISupportsString);
    alert(elt.data);
}

TOP

about:feeds の置換で自前のXUL製フィードビューアを使う

昨年開催された Firefox Developers Conference にて、 Firefox 2 で新たに搭載された Feed Content Access API を利用した独自フィードビューアの実装例を示したのだが、 Firefox 標準のフィードプレビュー (chrome://browser/content/feeds/subscribe.xhtml) を自前のXUL製フィードビューアに置き換える方法がわからず、やむを得ず自前のXUL製フィードビューアの chrome URL を「はてなRSS」や「livedoor reader」と同じようにWebサービスとして登録し、フィードを読み込むとその chrome URL へ遷移させることでビューアを置き換えるようにしていた。
(詳しくは FeedContentAccessAPI.pdf の「第二段階」を参照)

しかし、 nanto さんによる XPCOM コンポーネントの置換: Days on the Moon のやり方をそのまんま使って「about:feeds」を置換することで、前述のような遠回りなやり方をせずとも、いとも簡単に Firefox 標準のフィードプレビューを自前のXUL製フィードビューアに置き換えることに成功した。「about:feeds」を置換するにあたり、自分で考えてコードを書く必要のある部分はせいぜい nsIAboutModule の newChannel メソッドくらいのもので、以下のように自前のXUL製フィードビューアの chrome URL でチャネルを作って返すだけ。

newChannel: function(aURI) {
    var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
    var channel = ios.newChannel("chrome://sample/content/feedview.xul", null, null);
    channel.originalURI = aURI;
    return channel;
},

この方法のおいしいところは、 Feed Stream Converter 側でパースした結果を nsIFeedResultService 経由で自前のXUL製フィードビューア側から取り出して使うことができるという点である。前述の苦し紛れの方法(Webサービスとして登録した chrome URL へ遷移)ではそれが不可能なので、遷移した先のXULで XMLHttpRequest とかを使って改めてフィードの取得とパースをしてやる必要があった。

ちなみに nsIFeedResultService とは、 Feed Stream Converter がフィードをパースした結果をグローバルにアクセス可能なオブジェクトとして保持することで、実際にフィードの画面表示を行う Feed Writer から参照できるようにするための XPCOM である。 Feed Stream Converter がパース結果を nsIFeedResultService へ登録する処理の流れは FeedConverter.js の handleResult を見ればわかる。 Firefox 標準のフィードプレビューを使う設定になっている場合、つまり設定値「browser.feeds.handler」が「ask」の場合、 nsIFeedResultService#addFeedResult でパース結果を登録してから「about:feeds」のチャネルを開くのに対して、Webサービスを使用する設定の場合、つまり設定値「browser.feeds.handler」が「web」の場合、 nsIWebContentConverterService#loadPreferredHandler を使って新たな URI をロードし直すだけの処理となっている。

なお、 nsIFeedResultService からのパース結果の取り出し方は、 FeedWriter.js の _getContainer を見ればわかる。

var channel = window.QueryInterface(Ci.nsIInterfaceRequestor)
              .getInterface(Ci.nsIWebNavigation)
              .QueryInterface(Ci.nsIDocShell_MOZILLA_1_8_BRANCH)
              .currentDocumentChannel;
var feedSvc = Cc["@mozilla.org/browser/feeds/result-service;1"]
              .getService(Ci.nsIFeedResultService);
var result = feedSvc.getFeedResult(channel.originalURI);  // nsIFeedResult

TOP

ユーザのアイドル時間を測定する nsIIdleService

Firefox 3 (Minefield) 以降限定だが、 nsIIdleService を使ってユーザのアイドル時間を測定したり、ユーザが一定時間何も操作しなかったことを検知したり、さらにその状態から操作を開始したことを検知することができる。言わずもがな、メッセンジャーのようなアプリケーションでユーザが退席中かオンラインかを見分ける用途などに最適だ。

nsIIdleService

まずXPCOMサービスを取得する。

gIdleService = Components.classes["@mozilla.org/widget/idleservice;1"]
               .getService(Components.interfaces.nsIIdleService);

現在のアイドル時間を取得するなら、 idleTime プロパティを調べるだけ。単位はミリ秒である。

setInterval(function() {
    document.getElementById("idleTime").value = Math.round(gIdleService.idleTime / 1000) + " 秒";
}, 1000);

ユーザが○○分間何もしなかった(マウスを動かしたりキーボードに触れたりしなかった)ことを検知して何らかの処理を実行するためには、 nsIObserver インタフェースを実装したオブザーバを作る。 observe メソッドへ渡される引数は…

aSubject nsIIdleService 自身
aTopic 指定時間が経過してアイドル状態に突入した場合は「idle」
アイドル状態から通常状態へと戻った場合は「back」
aData 現在のアイドル時間
gIdleObserver = {
    observe: function(aSubject, aTopic, aData) {
        var status;
        switch (aTopic) {
            case "idle": status = " 退席中..."; break;
            case "back": status = " 戻りました"; break;
        }
        document.getElementById("log").value += new Date().toLocaleTimeString() + status + "
";
    }
};

オブザーバを addIdleObserver メソッドで登録する。第2引数はアイドル状態になるまでの時間(単位は分)である。

gIdleService.addIdleObserver(gIdleObserver, 1);

上記サンプルでは、1分間(厳密には+5秒のディレイあり)何も操作しないと「退席中…」が表示され、その後何か操作をすると「戻りました」が表示される。

用が済んだら removeIdleObserver でオブザーバを解除するのをお忘れなく。

gIdleService.removeIdleObserver(gIdleObserver, 1);

» mozilla/widget/public/nsIIdleService.idl

TOP

拡張機能が無効あるいは削除される前に何らかの処理を実行したい

ある拡張機能が無効あるいは削除される前に何らかの処理を実行したい場合がある。

拡張機能のインストールやアンインストール、無効・有効の変更の実際の処理は、 Firefox を起動してからブラウザのウィンドウが開かれるまでの間に行われるが、どうやらこのタイミングにフックして、無効あるいは削除対象の拡張機能の中の処理を実行することはできないもよう。

そのかわり、「ツール」→「アドオン」から「無効」あるいは「削除」ボタンをクリックすることである拡張機能を削除しようとするタイミングであれば、その拡張機能の中の処理を実行することは可能だ。拡張機能のインストール・アンインストール、有効・無効などの操作をしようとしたとき、「em-action-requested」というトピック名のグローバルな通知が送られるので、これを nsIObserver インタフェースを実装したオブザーバによって監視することで実現される。

トピック名「em-action-requested」のグローバルな通知が送られる時、observe メソッドに渡される引数から、どの拡張機能に対してどういう処理を行うかを判別できる。「削除」ボタンをクリックして拡張機能を削除しようとしたのであれば、3番目の引数は「item-uninstalled」である。したがってこのタイミングで、不要な設定値をリセットするなどの何らかの処理を実行させることが可能。ただし、一度削除しようとした後でも「キャンセル」ボタンをクリックして削除をやめることができるため、削除しようとした時点で処理を実行してしまうのは時期尚早といえる。この時点ではフラグを上げ下げするだけにして、実際に処理を行うのは Firefox を終了させる直前(トピック名 “quit-application” の通知が送信されるタイミング)まで待つといった工夫が必要だ。

問題点としては、「ツール」→「アドオン」からではなく、プロファイルフォルダの extensions フォルダから直接拡張機能のフォルダを削除してアンインストールした場合には対応できない点が挙げられる。

以下は “em-action-requested” の通知を監視する nsIObserver インタフェースを実装した独自XPCOMコンポーネントのコードの一部である。なお、 nsIModule::registerSelf() が呼ばれたタイミングで nsICategoryManager::addCategoryEntry() によってコンポーネントを “app-startup” カテゴリに登録することで、 Firefox 起動時(ブラウザのウィンドウが開く前)にオブザーバの登録処理を実行させることが可能となる。

_toBeDisabled: false,
_toBeUninstalled: false,

/**
 * nsIObserver
 */
observe: function(aSubject, aTopic, aData)
{
    var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
    switch (aTopic)
    {
        // Firefox起動時
        case "app-startup":
            // オブザーバを登録
            os.addObserver(this, "em-action-requested", false);
            os.addObserver(this, "quit-application", true);
            break;
        case "em-action-requested":
            const extid = "myextension@mysite.com";    // 拡張機能のID
            if (aSubject instanceof Ci.nsIUpdateItem && aSubject.id == extid)
            {
                switch (aData)
                {
                    case "item-disabled":
                        // 拡張機能を無効にしようとした
                        this._toBeDisabled = true;
                        break;
                    case "item-uninstalled":
                        // 拡張機能を削除しようとした
                        this._toBeUninstalled = true;
                        break;
                    case "item-cancel-action":
                        // 無効あるいは削除する処理をキャンセルした
                        this._toBeDisabled = false;
                        this._toBeUninstalled = false;
                        break;
                }
            }
            break;
        // Firefox終了時
        case "quit-application":
            // オブザーバの登録を解除
            os.removeObserver(this, "em-action-requested");
            os.removeObserver(this, "quit-application");
            if (this._toBeDisabled)
            {
                // 拡張機能を無効にする前に何らかの処理を実行
            }
            if (this._toBeUninstalled)
            {
                // 拡張機能を削除する前に何らかの処理を実行
            }
            break;
    }
},

TOP

nsIFeed から各種フィード情報を取得する

Firefox 2 に搭載された Feed content access API を使って、各種フィード情報を取得する。
以下、「feed」を nsIFeed 型オブジェクトとする。

フィードのタイトル

// マークアップされている場合にタグも含んだ文字列を取得する
var feedTitle = feed.title.text;
// マークアップされている場合にタグを除いた文字列として取得する
var feedTitle = feed.title.plainText();
// マークアップされている場合に node に対する DocumentFragment として取得する
var feedTitle = feed.title.createDocumentFragment(node);

フィードのサブタイトル

title, subtitle プロパティはともに nsIFeedTextConstruct 型を返すので、マークアップされている場合の取り扱いは title 同様。

var feedSubTitle = feed.subtitle.plainText();

フィードの最終更新日

nsIFeedContainer の updated プロパティは RFC822 形式の文字列を返すので、そのまま Date オブジェクトを生成することが可能。

var feedUpdated = new Date(feed.updated);

フィードの画像

RSS2.0 の image タグで記述されたフィードの画像を取得する。

var feedImage = feed.fields.getProperty("image").QueryInterface(Components.interfaces.nsIPropertyBag2);
// 画像のURL
feedImage.getPropertyAsAString("url");
// リンク先URL
feedImage.getPropertyAsAString("link");
// タイトル
feedImage.getPropertyAsAString("title");

リファレンス:
nsIFeed – MDC
nsIFeedContainer – MDC
nsIFeedTextConstruct – MDC
Interface Reference – nsIPropertyBag2

TOP

nsIFeedEntry から各種フィードエントリ情報を取得する

Firefox 2 に搭載された Feed content access API を使って、各種フィードエントリ情報を取得する。
以下、「entry」を nsIFeedEntry 型オブジェクトとする。

エントリのタイトル

フィードのタイトルと同様。

var title = entry.title.text;

エントリのパーマリンク

nsIFeedContainer の link プロパティは nsIURI 型を返す。

var parmaLink = entry.link.spec;

エントリの最終更新日

フィードの最終更新日と同様。

var lastUpdated = new Date(entry.updated);

エントリの本文

// RSS2.0の description タグで記述された内容を取得する
var summary = entry.summary;
// RSS2.0の content:encoded タグで記述された内容を取得する
var content = entry.content;
// node に対する DocumentFragment を生成する
var docFrag = (content || summary).createDocumentFragment(node);

エントリの筆者

for (var i = 0; i < entry.authors.length; i++)
{
    var author = entry.authors.queryElementAt(i, Components.interfaces.nsIFeedPerson);
    // 名前
    var authorName = author.name;
    // E-mail
    var authorMail = author.email;
    // 関連 URI(多くの場合ホームページ)
    var authorURI  = author.uri.spec;
}

エントリのカテゴリ

nsIFeedContainer の categories プロパティは使えないようだ。その代わり、以下のようにすることで成功。

var categories = entry.fields.getProperty("categories").QueryInterface(Components.interfaces.nsIArray);
for (var i = 0; i < categories.length; i++)
{
    var category = categories.queryElementAt(i, Components.interfaces.nsIPropertyBag);
    // カテゴリ名称
    var term = category.getProperty("term");
}

リファレンス:
nsIFeedPerson - MDC
Interface Reference - nsIArray

TOP

nsIScriptSecurityManager で危険なURIを除外する

nsIScriptSecurityManager の checkLoadURI や checkLoadURIStr メソッドによって、ある URI のページからリンクされる別の URI がポリシーに沿ったものであるかどうかを判別することができる。
以下のサンプルは、現在のURI (sourceURI) に対して、リンク先のURI (targetURI) が javascript: や data: プロトコルで表された危険が潜む可能性のある URI であるかどうかをテストしている。

var sourceURI = "http://www.example.com/";
var targetURI = "javascript:alert('Blah');";

var SECMAN = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
             .getService(Components.interfaces.nsIScriptSecurityManager);
// disallow javascript: and data: protocols
const flags = Components.interfaces.nsIScriptSecurityManager.DISALLOW_SCRIPT_OR_DATA;
try {
    SECMAN.checkLoadURIStr(sourceURI, targetURI, flags);
    alert("SAFE URI: " + targetURI);
} catch (ex) {
    // if the URI is unsafe, threw NS_ERROR_DOM_BAD_URI
    alert("UNSAFE URI: " + targetURI);
}

Firefox 2 で備わったフィードプレビュー機能では、見かけ上 http: プロトコルのXMLデータを表示させているように見えても実際にロードされているデータはクロム権限で動作する以下のXHTMLである。

chrome://browser/content/feeds/subscribe.xhtml

したがって、フィードのパーマリンクに javascript: プロトコルによる危険な URI が潜んでいる場合、ユーザがフィードのエントリをクリックすることでそのスクリプトが発動してしまうことになり兼ねない。
そこで、フィードプレビューを生成する際、 FeedWriter クラスの _safeSetURIAttribute メソッドが上記のように nsIScriptSecurityManager を利用し、 危険な URI を a要素等のhref属性等にセットしない(つまりリンクしない)仕組みになっている。

TOP

独自のプロトコルを追加する

以下のページで解説されている通りに JavaScript 製 XPCOM を登録してやることで、簡単に実現可能。
Adding a New Protocol to Mozilla

日本語訳
Latest topics > Firefoxで独自プロトコルを定義する方法 – outsider reflex

TOP

nsIStringBundleService

JavaScript内でローカライズされた文字列(平たく言えば日本語)を使用するには、properties ファイルを xul:stringbundle 要素から参照して getString や getFormattedString によって文字列を取り出す方法がある。これは XUL Tutorial にも記載されているように基本的な方法である。
しかし、ユーザーインターフェースとは切り離されたプログラム的な制御がメインの JavaScript にてローカライズされた文字列を使用する場合(特に自前のXPCOMコンポーネントを実装する場合)、いちいち xul へ stringbundle 要素を配置するのは面倒であり、規模が大きくなるとややこしくなりやすい。こういう場合には nsIStringBundleService が便利である。このXPCOMサービスは createBundle メソッドによって指定したURIから properties ファイルを読み込んで nsIStringBundle 型のXPCOMオブジェクトを生成し、 GetStringFromName や formatStringFromName メソッドによってローカライズされた文字列を取り出したりすることができる。以下のサンプルにある getLocaleString 関数は、 BookmarksUtils を参考にして文字列の置換の有無に両対応したものである。

リファレンス:
Interface Reference – nsIStringBundleService
Interface Reference – nsIStringBundle

sample.properties

HELLO=こんにちは
MY_NAME_IS=私の名前は%Sです。

sample.js

var FoxkehUtils = {

    _stringBundle : null,

    getLocaleString : function(aStringKey, aReplacements)
    {
        // 初めて呼び出された時に properties ファイルを読み込んで nsIStringBundle オブジェクト生成
        if ( !this._stringBundle ) {
            const BUNDLE_SVC = Components.classes['@mozilla.org/intl/stringbundle;1'].getService(Components.interfaces.nsIStringBundleService);
            this._stringBundle = BUNDLE_SVC.createBundle("chrome://sample/locale/sample.properties");
        }
        try {
            if ( !aReplacements )
                // 置換なし
                return this._stringBundle.GetStringFromName(aStringKey);
            else
                // 置換あり
                return this._stringBundle.formatStringFromName(aStringKey, aReplacements, aReplacements.length);
        } catch(ex) {
            // 未定義の場合 fallout
            return aStringKey;
        }
    },

    say : function()
    {
        alert(this.getLocaleString("HELLO"));    // こんにちは
        alert(this.getLocaleString("MY_NAME_IS", ["フォクすけ"]));    // 私の名前はフォクすけです。
    },
}

FoxkehUtils.say();

TOP