Add-on SDK で始めるアドオン開発( XPCOM 編)

前回の記事:Add-on SDK で始めるアドオン開発(自作モジュール編)

XPCOM コンポーネントの利用

前回までの記事では、 Add-on SDK に備わっているライブラリを用いたアドオンの実装を行いました。
SDK のライブラリを用いた開発は、簡単な記述で拡張機能を実装できる反面、必要なモジュールが不足しているなどの理由で、実現可能な機能が限られているのが現状です。

XPCOM コンポーネントは、ファイルの入出力やレジストリ操作など、 JavaScript だけでは実現できない機能をクロスプラットフォームに提供しており、これを用いることでより高機能な拡張機能や、 SDK のモジュールなどを実装することが可能となります。

今回の記事では、 Firefox のデータベースからユーザの履歴情報を取得する機能を例として、XPCOM コンポーネントの機能を SDK で利用する方法を紹介します。

Add-on SDK から XPCOM コンポーネントを利用する際には、最初に次のような宣言を行う必要があります。

const {Cc, Ci} = require("chrome");

ここで、 Cc は Components.classes, Ci は Components.interfaces を表すエイリアスです。Add-on SDK では、直接 XPCOM のコンポーネントにアクセスすることはできないので、必ず require(“chrome”) を用いて参照を行います。

ファイルを扱う

XPCOM コンポーネントを用いると、ローカルファイルの操作や入出力を行うことができます。
次のコードは、ローカルの Firefox プロファイルディレクトリと、その中の places.sqlite ファイルのパスを表示させるコードです。

main.js

const {Cc, Ci} = require("chrome");

var file = Cc['@mozilla.org/file/directory_service;1'].
           getService(Ci.nsIProperties).
           get('ProfD', Ci.nsIFile);
console.log("ProfD: " + file.path);

file.append('places.sqlite');
console.log("places: " + file.path);

file には nsIFile オブジェクトが格納され、 path プロパティに、そのフォルダやファイルのパスが保存されています。
‘ProfD’ はプロファイルディレクトリを表すキーワードです。
(その他の利用可能なキーワードはこちら
フォルダ内のファイルを取得するには、 nsIFile.append() メソッドを用いてファイル名を指定します。

データベースから履歴情報を取得

先ほどパスを表示した places.sqlite ファイルは、 Firefox の Web 履歴やブックマーク情報を記録している SQLite データベースの保存ファイルです。
Firefox のデータベースには、その他にフォームや検索語の履歴を記録した formhistory.sqlite や、cookie 情報を記録した cookies.sqlite などがあります。
SQLite Manager などのアドオンを利用すると、これらのデータベースを簡単に閲覧することができます。

SQLite Manager

今回は、この places データベースに XPCOM の機能を用いてアクセスする方法を紹介します。
まず、最新 10 件の Web 履歴をデータベースから取得し、コンテクストメニューに表示させる拡張機能を実装してみます。

前回までのコンテクストメニューの拡張機能パッケージで、 lib フォルダに places.js ファイルを作成し、次のようなコードを記述します。

places.js

const {Cc, Ci} = require("chrome");

var file = Cc['@mozilla.org/file/directory_service;1'].
           getService(Ci.nsIProperties).
           get('ProfD', Ci.nsIFile);
file.append('places.sqlite');

const db = Cc['@mozilla.org/storage/service;1'].
           getService(Ci.mozIStorageService).
           openDatabase(file);

exports.getLatestHistoriesFromDB = function (maxNum){
    var histories = [];
    var statement = db.createStatement(<![CDATA[
            SELECT  p.title, p.url, h.visit_date/1000
            FROM moz_places p, moz_historyvisits h
            WHERE h.place_id = p.id
            ORDER BY h.visit_date DESC
            LIMIT ?1;
            ]]>.toString());
    try{
        statement.bindInt64Parameter(0, maxNum);
        while (statement.executeStep()){
            var historyItem = {
                title: statement.getString(0),
                url: statement.getString(1),
                time: statement.getInt64(2)
            };
            histories.unshift(historyItem);
        }
    }
    finally{
        statement.reset();
    }
    return histories;
}

先ほど取得した places.sqlite を示す nsIFile オブジェクトから、 mozIStorageService を用いてデータベースに接続します(8~10行目)。
続いて、 createStatement メソッドを用いて、データベースに問い合わせを行うための SQL 文を生成します(14~20行目)。

SQL 文では SELECT 句で取得する列名、 FROM 句で対象テーブル名、 WHERE 句でテーブル間の連結や絞り込み条件を指定します。
visit_date 列は、履歴の訪問日時を 1970 年 1 月 1 日からの経過マイクロ秒 (PRTime 形式 ) で格納しており、 JavaScript 側での変換のために 1000 で割っています。

ここで、 「?1」 などの表現は、あとから値を挿入するためのプレースホルダです。
このプレースホルダに整数値を挿入する場合には、 bindInt64Parameter メソッドなどを用います。
プレースホルダのインデックスは 1 から始まりますが、バインドの際のインデックスは 0 から始まることに注意してください。

この問い合わせを executeStep メソッドで実行(23行目)し、getString メソッドなどを用いて値を取得しています。
次に、 histoy-menu.js へ以下のようなコードを追記します。

history-menu.js

…
var historyMenu;

exports.createNewHistoryMenu = function(histories){
    if(historyMenu !== undefined) historyMenu.destroy();
    var pageItems = [];
    var menuLabel = bundle.get("storageMenuLabel");
    var script = 'self.on("click", function (node, data) { ' +
                 '    location.href = data;' +
                 '});';
    var requirements = {
        title: { map: function(v) v ? v.toString() : "No Title" },
        url:   { map: function(v) v.toString() },
        time:  { map: function(v) new Date(v).toLocaleString() }
    };
    histories.forEach(function(history){
        var validatedHistory = apiUtils.validateOptions(history, requirements);
        pageItems.push(
        contextMenu.Item({
            label: '[' + validatedHistory.time + '] ' + validatedHistory.title,
            data : validatedHistory.url
        }));
    });
    historyMenu = contextMenu.Menu({
        label: menuLabel,
        contentScript: script,
        items: pageItems
    });
}

これは、先ほど定義した getLatestHistoryFromDB メソッドで取得した Web 履歴の配列から、新しいコンテクストメニューを作成するメソッドです。
前回説明した validateOptions メソッドの部分で、ミリ秒形式の訪問日時をロケール文字列形式に変換しています(14, 17行目)。

最後に、これらのメソッドを main.js で呼び出します。

main.js

const historyMenu = require("history-menu");
const places = require("places");
const PageMod = require("page-mod").PageMod;

exports.main = function(){
    var maxItemLength = 10;

    PageMod({
        include: ["*"],
        onAttach: function onAttach(worker) {
            historyMenu.createNewHistoryMenu(places.getLatestHistoriesFromDB(maxItemLength));
        }
    });

}

このプログラムを実行すると、最近訪問した Web ページへのリンクが最大 10 件までコンテクストメニューに表示されます。

閲覧ページのセッション ID を取得

places データベースの Web 履歴には、 Web ページの遷移元ページや、一連の遷移を一つにまとめたセッション番号などの情報も記録されています。
この情報を利用して、 Firefox のそれぞれのタブごとに固有のセッション履歴を表示するように、コンテクストメニューを拡張してみます。

まず、現在のページのセッション番号を取得する機能を実装します。
places.js に次のようなコードを追記します。

places.js

…
exports.getSessionIdByURL = function(url){
    var sessionId;
    var statement = db.createStatement(<![CDATA[
        SELECT h.session
        FROM moz_historyvisits h, moz_places p
        WHERE h.place_id = p.id
        AND url = ?1
        ORDER BY h.visit_date DESC
        LIMIT 1;
        ]]>.toString());
    try{
        statement.bindStringParameter(0, url);
        if(statement.executeStep()){
            sessionId = statement.getInt64(0);
            console.log("sessionId: " + sessionId);
        }
    }
    finally{
        statement.reset();
    }
    return sessionId;
}

このメソッドは、 URL を入力として、その URL と一致する履歴項目のうち、最も新しいもののセッション番号を取得するメソッドです。

このメソッドを main.js から呼び出します。
main.js に、次のようなコードを追記します。

main.js

…
const tabs = require("tabs");
…
exports.main = function(){
    …
    tabs.on('activate', function(tab){
        places.getSessionIdByURL(tab.url);
    });
}

タブが切り替えられたタイミングで、そのタブのページのセッション番号を取得できるように、 tabs モジュールを利用しました。

このプログラムを実行すると、タブを切り替えたときに、コンソール画面にセッション番号が出力されます。

セッション毎の履歴情報を取得

取得したセッション番号から、そのセッションの Web 履歴をすべて取得するのが、以下のコードです。

places.js

…
exports.getSessionHistoriesFromDB = function (sessionId){
    var histories = [];
    var statement = db.createStatement(<![CDATA[
        SELECT DISTINCT p.title, p.url, h.visit_date /1000
        FROM moz_places p, moz_historyvisits h
        WHERE h.place_id = p.id
        AND h.session = ?1
        AND h.visit_date > (strftime('%s', 'now') - 60 * 60 * 24) * 1000 * 1000
        ORDER BY h.visit_date DESC
        ]]>.toString());
    try{
        statement.bindInt64Parameter(0, sessionId);
        while (statement.executeStep()){
            let historyItem = {
                title: statement.getString(0),
                url: statement.getString(1),
                time: statement.getInt64(2)
            };
            histories.unshift(historyItem);
        }
    }
    finally{
        statement.reset();
    }
    return histories;
}

places データベースのセッション番号は、過去のセッションと同一の番号が割り当てられることがあるため、SQL 文の 9 行目で、 24 時間以内のページのみを取得するように指定しています。

main.js

const historyMenu = require("history-menu");
const places = require("places");
const tabs = require("tabs");
const PageMod = require("page-mod").PageMod;

exports.main = function(){
    function newSessionMenu(url){
        let sessionId = places.getSessionIdByURL(url);
        historyMenu.createNewHistoryMenu(places.getSessionHistoriesFromDB(sessionId));
    }

    PageMod({
        include: ["*"],
        onAttach: function onAttach(worker) {
            newSessionMenu(worker.tab.url);
        }
    });

    tabs.on('activate', function(tab){
        newSessionMenu(tab.url);
    });
} // end of exports.main

実行例

このように、 Add-on SDK で XPCOM コンポーネントの機能を用いて、プロファイルデータベースから情報を取得することができました。

XPCOM コンポーネントでは、他にも豊富な機能が提供されています。詳細は XPCOM API Reference などを参照してください。

次回:未定