Now browsing the SCRAPBLOG weblog archives.

[userChrome.css] Places Style Throbber

Firefox 3 のスマートロケーションバー入力中にまれに一瞬だけ表示される回転するアイコンがかっこいいので、読み込み中のタブやアドオンマネージャの更新チェック中などの Throbber アイコンに転用するための userChrome.css を作った。ただし、回転が停止しているバージョンのアイコンは今までどおりです。

Places Style Throbber Places Style Throbber

userChrome.css

/* ::::: Places Style Throbber ::::: */

#navigator-throbber[busy="true"],
toolbar[iconsize="small"] #navigator-throbber[busy="true"],
toolbar[mode="text"] #navigator-throbber[busy="true"],
.tabbrowser-tab[busy] > .tab-icon-image,
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon,
#sidebar-throbber[loading="true"],
#checkForUpdates[loading="true"],
#extensionsManager richlistitem[loading="true"] .updateBadge,
#extensionsManager .addonThrobber,
#extensionsManager .throbber {
    list-style-image: url("chrome://browser/skin/places/searching_16.png") !important;
}

TOP

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

[Places] フォルダ内のブックマークを列挙する

Places データベースへの問い合わせ結果から得たブックマークのノードは nsINavHistoryResultNode インタフェースを実装するオブジェクトであるが、ブックマークフォルダは nsINavHistoryContainerResultNode インタフェースも合わせて実装しており、 childCount プロパティで子ノードの数を調べたり、 getChild メソッドで指定したインデックスの子ノードを取得することができる。
例えば、[Places] ビューと nsIPlacesView インタフェース の続きとして、右クリックしたブックマークフォルダ内の子ノードにアクセスするには、以下のようにすればよい。ただし、ビュー上でフォルダが開いていることを前提とする。

// assume that node is instanceof Ci.nsINavHistoryContainerResultNode and is open.
for (var i = 0; i < node.childCount; i++) {
    var childNode = node.getChild(i);
}

一方、ビュー上でフォルダが閉じた状態になっている場合、 childCount プロパティなどを使って子ノードへアクセスしようとすると NS_ERROR_NOT_AVAILABLE 例外がスローされる。そこで、ブックマークフォルダのノードの containerOpen プロパティへ true をセットすることで一時的にフォルダを開いてから子ノードへアクセスし、処理が終わった後に false をセットしてフォルダを閉じるようにする。以下は、引数で指定したブックマークフォルダ内のすべてのブックマークを再帰的に取得し、配列として返す関数である。

function flatChildNodes(aNode) {
    var ret = [];
    var closeOriginally = !aNode.containerOpen;
    if (closeOriginally)
        // if the folder is closed, open it.
        aNode.containerOpen = true;
    for (var i = 0; i < aNode.childCount; i++) {
        var childNode = aNode.getChild(i);
        if (PlacesUtils.nodeIsBookmark(childNode))
            ret.push(childNode);
        else if (PlacesUtils.nodeIsFolder(childNode) && 
                 !PlacesUtils.nodeIsLivemarkContainer(childNode))
        // call this function recursive
        ret = ret.concat(arguments.callee(childNode));
    }
    if (closeOriginally)
        // don't forget to restore the folder's original closed state
        aNode.containerOpen = false;
    return ret;
}

上記の例では、ビュー上に表示されているブックマークフォルダ、つまりデータベースへの問い合わせ結果が取得済みのブックマークフォルダを対象としていた。ビューへの表示が無い場面でフォルダ内のノードへアクセスするには、データベースへ問い合わせを行う必要がある。通常 Places データベースへの問い合わせを行うには、 nsINavHistoryService#getNewQuery でクエリオブジェクトを生成し executeQuery で実行するという手続きが必要であるが、 PlacesUtils.getFolderContents を使えばより単純なコードでフォルダ内のノードへアクセス可能である。

var result = PlacesUtils.getFolderContents(node.itemId);    // nsINavHistoryResult
var parentNode = result.root;
for (var i = 0; i < parentNode.childCount; i++) {
    var childNode = parentNode.getChild(i);
}

以下は、ブックマークフォルダの itemId プロパティを引数に、そのフォルダ内の全ブックマークを再帰的に取得して配列として返す関数である。

function flatChildNodes(aItemId) {
    var ret = [];
    var parentNode = PlacesUtils.getFolderContents(aItemId).root;
    for (var i = 0; i < parentNode.childCount; i++) {
        var childNode = parentNode.getChild(i);
        if (PlacesUtils.nodeIsBookmark(childNode))
            ret.push(childNode);
        else if (PlacesUtils.nodeIsFolder(childNode) && 
                 !PlacesUtils.nodeIsLivemarkContainer(childNode))
            // call this function recursive
            ret = ret.concat(arguments.callee(childNode.itemId));
    }
    return ret;
}

TOP

[Places] nsINavHistoryResultNode からブックマークの各種情報を取得する

[Places] ビューと nsIPlacesView インタフェースから引き続き、 Places データベースからの検索結果 (nsINavHistoryResult オブジェクト) から得たブックマークのノード (nsINavHistoryResultNode オブジェクト) について、各種情報を取得する。

ブックマークの種類

あるノードの種類を調べる、例えばフォルダかどうかを調べるには、 type プロパティを調べる。

if (node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER)
    alert("The node is a folder.");

PlacesUtils には nodeIs* メソッドがたくさん用意されているので、こちらを使った方が便利。

if (PlacesUtils.nodeIsFolder(node))
    alert("The node is a folder.");
else if (PlacesUtils.nodeIsLivemarkContainer(node))
    alert("The node is a Live Bookmark.");

ブックマークのタグ

nsINavHistoryResultNode の tags プロパティではすべてのタグが「, 」で連結された文字列として取得できる。

var tags = node.tags;

一方、 nsITaggingService#getTagsForURI ですべてのタグを配列として取得することも可能。なお、 PlacesUtils.tagging は nsITaggingService へのショートカット、 PlacesUtils._uri は URI 文字列から nsIURI オブジェクトを生成する便利メソッドである。

var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(node.uri), {});

ブックマークのアノテーション

Places では、ブックマークの「説明」プロパティなどは、ブックマークアイテムに対するアノテーションのひとつとして管理されている。アノテーションを取得するには nsIAnnotationService#getItemAnnotation を使う。 PlacesUtils.annotations は nsIAnnotationService へのショートカット、 DESCRIPTION_ANNO はアノテーション名 “bookmarkProperties/description” である。

var desc = PlacesUtils.annotations.getItemAnnotation(node.itemId, DESCRIPTION_ANNO);

以下のように PlacesUIUtils の便利メソッドを使ってブックマークの itemId に対応する「説明」プロパティを取得することも可能。

var desc = PlacesUIUtils.getItemDescription(node.itemId);

ブックマークのアイコン

nsINavHistoryResultNode の icon プロパティから nsIURI オブジェクトとして取得可能。

var iconLink = node.icon ? node.icon.spec : null;

取得した URI は「moz-anno:favicon:http://en-us.www.mozilla.com/favicon.ico」のような特殊なプロトコルで示される。この URI をブラウザに直接貼り付けてみればわかるように、「moz-anno:」プロトコルはプロトコルハンドラによって自動的にアイコンの画像データへ変換される。

一方、あるブックマークの URL を引数としてアイコンの URL を取得するには、 nsIFaviconService#getFaviconForPage を使用する。 PlacesUtils.favicons は nsIFaviconService へのショートカットである。

var icon = PlacesUtils.favicons.getFaviconForPage(PlacesUtils._uri(node.uri));

アイコンの MIME 型と画像データを取得するには、 nsIFaviconService#getFaviconData を使用する。

var mimeType = {};
var iconData = PlacesUtils.favicons.getFaviconData(icon, mimeType, {});
mimeType = mimeType.value;

BASE64 形式の data: URI に変換するには以下のようにする。

var dataURI = "data:" + mimeType + ";" + "base64," + btoa(String.fromCharCode.apply(null, iconData));

TOP

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

nsIPlacesView インタフェース

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

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

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     : " + node.title + "
" + 
    "uri       : " + node.uri + "
" + 
    "type      : " + node.type + "
" + 
    "icon      : " + (node.icon ? node.icon.spec : "") + "
" + 
    "itemId    : " + node.itemId + "
" + 
    "tags      : " + 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