[Places] ビューと nsIPlacesView インタフェース

nsIPlacesView インタフェース

ブックマークや履歴といった Places データベースに保持されている内容は、ツリー/メニュー/ツールバーといった色々な GUI ウィジェット(「places view」あるいは単に「ビュー」と呼ぶ)として実際に目に見える形で表示される。各ビューはいずれも XBL にて nsIPlacesView インタフェースで定められた各種プロパティ・メソッドを実装しており、ビューの違いを意識することなくコントローラ側で各種機能を実装できる設計となっている。

Places ではデータベースへの問い合わせ結果を所定のインタフェースを介してビューに結びつけて表示させる。問い合わせ結果全体を表す nsINavHistoryResult オブジェクトからは、個々の「行」に対応する nsINavHistoryResultNode オブジェクトへアクセス可能である。このオブジェクトを「result node」あるいは単に「ノード」と呼ぶ。あるビュー上でユーザが現在選択している項目に対応するノードは、 nsIPlacesView インタフェースの selectedNode プロパティによって取得可能である。

placesUIOverlay.js

引き続き、 [Places] 右クリックメニューへのメニュー項目追加 にて新たに追加した「Show Information」メニュー項目をクリックした際に実行される showBookmarkInformation 関数を実装する。

まずは右クリックメニューの対象となるビューと、そのビューにて選択しているノードを取得する。ここで、対象のビューとしてブックマークサイドバーのツリーだけを考慮すると、うっかり以下のようにやってしまうところである。

var view = document.getElementById("bookmarks-view");    // XULElement
var node = view.selectedNode;    // nsINavHistoryResultNode

しかし、これではせっかくのビューの違いを意識しない Places の設計が台無しである。ツリー/メニュー/ツールバーすべてを考慮して右クリックメニューの対象となっているビューを取得するには、以下のようにすればよい。 PlacesUIUtils.getViewForNode は、引数に指定した DOM ノードの先祖をたどって直近のビューを見つけ出す便利メソッドである。

var view = PlacesUIUtils.getViewForNode(document.popupNode);    // XULElement
var node = view.selectedNode;    // nsINavHistoryResultNode

あとはノードの各プロパティの情報を表示するだけ。各プロパティの詳細は nsINavHistoryService.idl 参照。

alert(
    "title \t: " + node.title + "\n" +
    "uri   \t: " + node.uri + "\n" +
    "type  \t: " + node.type + "\n" +
    "icon  \t: " + (node.icon ? node.icon.spec : "") + "\n" +
    "itemId\t: " + node.itemId + "\n" +
    "tags  \t: " + node.tags
);

TOP

[Places] 右クリックメニューへのメニュー項目追加

Firefox 2 でのブックマークの右クリックメニューは、ポップアップを表示するたびに JavaScript によって menuitem 要素を動的に生成する実装方式であったため、拡張機能によってメニュー項目を追加しづらいという問題があった。しかし、 Firefox 3 にて Places として実装が一新され、右クリックメニューの menuitem 要素や呼び出される command 要素は placesOverlay.xul という XUL ファイルで実装する形になり、拡張機能からのメニュー項目追加がしやすくなった。

Places の右クリックメニューは、以下の5つの場所で使用される。

ブックマークメニュー chrome://browser/content/browser.xul
ブックマークツールバー
ブックマークサイドバー chrome://browser/content/bookmarks/bookmarksPanel.xul
履歴サイドバー chrome://browser/content/history/history-panel.xul
履歴とブックマークの管理 chrome://browser/content/places/places.xul

各 XUL はいずれも placesOverlay.xul (chrome://browser/content/places/placesOverlay.xul) をオーバーレイしている。そこで、拡張機能から placesOverlay.xul に対してさらにオーバーレイして menuitem 要素などを追加することで、上記すべての場所の右クリックメニューへ一括してメニュー項目を追加することが可能となる。

サンプル

例として、ブックマークの右クリックメニューへ「Show Information」という新しいメニュー項目を追加する。
Places Context Menu

chrome.manifest

overlay    chrome://browser/content/places/placesOverlay.xul    chrome://myext/content/placesUIOverlay.xul

placesUIOverlay.xul

<popup id="placesContext">, <commandset id="placesCommands"> をマージポイントとして menuitem 要素と command 要素を追加する。

<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

    <script type="application/x-javascript" src="placesUIOverlay.js" />

    <commandset id="placesCommands">
        <command id="placesCmd_showInfo" oncommand="showBookmarkInformation();" />
    </commandset>

    <popup id="placesContext">
        <menuseparator insertafter="placesContext_openSeparator" />
        <menuitem id="placesContext_showInfo"
                  command="placesCmd_showInfo"
                  label="Show Information"
                  insertafter="placesContext_openSeparator"
                  selectiontype="single"
                  selection="bookmark|folder"
                  forcehideselection="livemarkChild|livemark/feedURI|PlacesOrganizer/OrganizerQuery" />
    </popup>

</overlay>

ブックマークの右クリックメニューは、右クリックした対象が通常のブックマークか、フォルダか、区切りか、などの条件によってメニュー項目数が増減する。メニュー項目を表示する条件は、 menuitem 要素へ追加された以下の4つの特殊な属性の値によって決定される。詳細は PlacesController クラスの buildContextMenu や _buildSelectionMetadata のコメントを参照。

属性 役割
selectiontype single, multiple のいずれかの値をセットすることで、選択している対象が単数か複数かの条件によってメニュー項目を表示する。
selection any, bookmark, folder, separator といった値を指定することで、選択している対象がブックマークか、フォルダか、区切りかといった条件でメニュー項目を表示する。 | 区切りで複数指定可能。
forcehideselection selection 属性の逆で、選択している対象の種類によってメニュー項目を非表示にする。
hideifnoinsetionpoint よくわからん。

上記の例では、単一のブックマークあるいはフォルダを選択しているときにだけ「Show Information」メニュー項目を表示する。ただし、例外としてライブブックマークや特殊な「ブックマークツールバー」フォルダを選択しているときはメニュー項目を表示しない。
また、上記の例ではメニューの区切り(menuseparator 要素)も追加しているが、ありがたいことに Places の右クリックメニューは連続するメニューの区切りを自動的に1個にして表示してくれるので、新たに追加するメニューの区切りについては、どういった条件で表示するのかを意識する必要が無い。

placesUIOverlay.js

function showBookmarkInformation() {
    // TODO
}

[Places] ビューと nsIPlacesView インタフェース へつづく…

TOP

McCoy を使ったアドオンの安全な更新方法の提供

Firefox 3 では、安全な更新方法が提供されていないアドオン、つまりインストールマニフェスト中の em:updateURL の値が https ではなく http プロトコルであるようなアドオンは、デフォルトでインストール不可となる。
どうしても em:updateURL の値を http プロトコルにしたければ、インストールマニフェストとアップデートマニフェストの両者へ以下のような細工を施す必要がある。

作業 ファイル 内容
#1 インストールマニフェスト
(install.rdf)
公開鍵を em:updateKey エントリとして追加する。
#2 アップデートマニフェスト
(update.rdf)
em:updateLink で示された XPI ファイルのハッシュ値を em:updateHash エントリとして追加する。
#3 マニフェストファイルに対する電子署名を em:signature エントリとして追加する。

このような細工を施すことで、以下のような仕組みにより安全な更新が可能となる。

  • アドオンマネージャで「更新の確認」を行った際、インストールマニフェスト中の公開鍵と、安全ではない経路からダウンロードしたアップデートマニフェスト中の電子署名を照合し、そのアップデートマニフェストの正当性を検証する。
  • アップデートマニフェストで示された新しいバージョンをインストールする際、アップデートマニフェスト中のハッシュ値と実際にダウンロードした XPI ファイルから算出したハッシュ値とを比較し、その XPI ファイルの正当性を検証する。

事前準備

上記作業#1~#3を実施するにあたり、事前に McCoy というアプリをインストールしておく。

作業#1の手順

  1. McCoy で「Create」ボタンを押して公開鍵(と秘密鍵)を生成する。
  2. 右クリックメニューの「Copy Public Key」から公開鍵をクリップボードにコピーする。
  3. 既に作成済みのインストールマニフェストへ <em:updateKey>{公開鍵の値}</em:updateKey> という行を追加する。

こうしてできあがったインストールマニフェストを含めて XPI ファイルを生成すれば、とりあえずはそのアドオンは Firefox 3 にインストール可能となる。なお、手順1で作成した公開鍵(と秘密鍵)は今後も同じものを使い続けるので、作業#1は一度実施すれば今後のバージョンアップ時はそのままで良い。
なお、手順1で作成した公開鍵(と秘密鍵)は、他のアドオンに対して使いまわしても構わない。

作業#2の手順

  1. XPI ファイルの SHA1 ハッシュ値を求める。
    SHA1 ハッシュの求め方は、 Win 用アプリ bkhashes, Firefox 拡張機能 MDHashTool, Cygwin の sha1sum コマンド、PHP の sha1_file 関数などがある。
  2. 既に作成済みのアップデートマニフェストへ <em:updateHash>sha1:{ハッシュの値}</em:updateHash> という行を追加する。
    ただし、 XML 構造によっては em:updateHash="sha1:{ハッシュの値}" という記述となる場合もある。

SHA1 ハッシュ値は、 XPI ファイルが1バイトでも違えばまったく異なる値となるので、 XPI ファイルをパッケージングするたびに作業#2を実施する必要がある。

作業#3の手順

  1. McCoy にて作業#1で作成した公開鍵を選択し、「Sign」ボタンを押して作業#2で作成したアップデートマニフェストのファイルを選択する。
  2. アップデートマニフェストへ自動的に em:signature エントリが追加される。

McCoy によって自動生成されたアップデートマニフェストは、XML の構造が少し複雑である。だが、このファイルを手動で編集してはならない。ちょっとでも編集すると、アドオンマネージャにて更新情報を検知できなくなってしまう。
自分は XML 構造が変わるのが嫌なので、手動で編集するためのアップデートマニフェストと、 McCoy で電子署名を付加したアップデートマニフェストとを別々に管理している。
なお、当然ながら作業#2で em:updateHash の値を書き換えたら、その都度作業#3を実施する必要がある。

動作確認手順

実際にアドオンマネージャにて更新確認と新しいバージョンのインストールの動作確認を行うための手順を示す。

  1. インストールマニフェストに対して作業#1を実施する。
  2. 1. のインストールマニフェストを含めた XPI インストーラを作成し、 Firefox へインストールする。
  3. 1. のインストールマニフェストの em:version の値を上げる。
  4. 3. のインストールマニフェストを含めた XPI インストーラを作成する。
  5. 4. の XPI インストーラから SHA1ハッシュを生成し、アップデートマニフェストに対して作業#2を実施する。
  6. 5. のアップデートマニフェストに対して作業#3を実施する。
  7. 4. と 6. の2ファイルを Webサーバへデプロイする。
  8. アドオンマネージャにて「更新を確認」する。

参考

Latest topics > XML署名とハッシュを使って安全な方法でアドオンを更新できるようにする - outsider reflex
くでんな日々や公開どう? ◆ [Mozilla] さぁ、McCoyをはじめよう!!
Install Manifests - MDC
Extension Versioning, Update and Compatibility - MDC
McCoy - MDC

TOP

[userChrome.css] ブックマークメニューのショートカットキーを表示しない

ブックマークメニューの「ブックマークの管理...」の横にある「Ctrl+Shift+B」のようなショートカットキーの表示が意外と幅を取って邪魔なので、 userChrome.css にて非表示にする。下記コードは Firefox 3 専用。

userChrome.css

/* [Firefox3] ブックマークメニューのショートカットキーを表示しない */
#bookmarksMenuPopup .menu-accel {
    display: none !important;
}

適用前

適用後

TOP

contenteditable 属性

position: absolute; な要素に対して Firefox 3 で導入された contenteditable="true" 属性を付加すると、内容の編集以外にもその要素をドラッグ&ドロップで位置を変更したり、リサイズしたりすることが可能。
テストケース

Firefox 3 でのセキュリティに関する変更 (Web ページからの chrome コンテントへのアクセスがデフォルトで制限される) 影響で、 ScrapBook の付箋アノテーション機能が壊れていた。 chrome.manifest で contentaccessible フラグを使えばこの制限を解除できるが、せっかくなのでこの機会に前から考えていた contenteditable 属性を使って付箋アノテーション機能を改善してみた。

TOP

[userChrome.css] ダウンロードマネージャのファイル名の文字サイズを調整

Firefox 3 で一新されるダウンロードマネージャで、ファイル名の文字サイズがやや大きめになっている。日本語版 Windows XP ではどうもしっくりこない気がするので、 userChrome.css によって普通の文字サイズに変更し、ついでに太字で強調する。

userChrome.css

/* [Firefox3] ダウンロードマネージャのファイル名 */
#downloadView label.name {
    font-size: 1em !important;
    font-weight: bold;
}

適用前

適用後

TOP

[userChrome.css] ロケーションバーのポップアップのタイトルの文字サイズを調整

Firefox 3 の新機能のひとつである、ロケーションバーに入力した文字列にマッチする候補を表示する機能(いわゆる awesomebar)で、ポップアップ中のタイトルの文字サイズがやや大きめになっている。日本語版 Windows XP だとどうもしっくりこない気がするので、 userChrome.css によって普通の文字サイズに変更する。

userChrome.css

/* [Firefox3] ロケーションバーのポップアップのタイトルの文字サイズ */
#PopupAutoCompleteRichResult .ac-normal-text {
    font-size: 1em !important;
}

適用前

適用後

TOP

[Exception... "'Component is not available' when calling method: [nsIHandlerService::getTypeFromExtension]

2008/5/3 追記
この問題は解決済みです。

Firefox 3にて、XULから <script type="application/x-javascript" src="chrome://myext/content/test.js" /> のようにして JavaScript を読み込む際、その JavaScript ファイルがjar形式アーカイブの中身ではなくて純粋なローカルファイルである場合(つまり chrome.manifest にて content myext content/myext/ のようにしている場合)、以下の例外がエラーコンソールに出力される。

エラー: [Exception... "'Component is not available' when calling method: [nsIHandlerService::getTypeFromExtension]" nsresult: "0x80040111 (NS_ERROR_NOT_AVAILABLE)" location: "" data: no]

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9pre) Gecko/2008042606 Minefield/3.0pre

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\n");
        if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
            dump("stopped\n");
    },
    onProgressChange: function (aWebProgress, aRequest,
                                aCurSelfProgress, aMaxSelfProgress,
                                aCurTotalProgress, aMaxTotalProgress) {
        dump("downloading... " + aCurSelfProgress + "/" + aMaxSelfProgress + "\n");
    },
    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 + "\n");
    },
    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