MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

ページの読み込みの検出

ウェブページとそのコンテンツの読み込みを検出して割り込むにはいくつかの方法があります。この読み込みは、それが起こった時、コンテンツが変更された時、またはその読み込みをブロックして代わりに何かを行った時だけ検出できます。ここで紹介するいくつかのテクニックは、メインのブラウザ領域へのコンテンツの読み込みにのみ適用されます。同時に、コンテンツが他の XUL ウィンドウに読み込まれる時や XUL コンテンツが読み込まれていることを検出します。また、別のテクニックでは、読み込み処理の異なるステップに割り込めます。どれを使用すべきかは、あなたの必要に応じて選んでください。ここでは、一般的に使用される最も簡単なテクニックから始めます。

アドオンとページの読み込み時のパフォーマンスはとても重要です。このセクションの内容を実装する予定のあるときは、パフォーマンスについて書かれた 付録 A をよく読んでください。

簡単な方法: Load イベント

次のコードは、tabbrowser コードのスニペットのページからのものです。簡単に説明すると、オーバーレイ内の chrome コードから load イベントのためのイベントリスナーを追加します。

this._loadHandler = function() { that._onPageLoad(); };

gBrowser.addEventListener("load", this._loadHandler, true);

gBrowser は、メインのブラウザウィンドウ内の tabbrowser 要素に対応するグローバルオブジェクトです。これは、いつでも役立つ関数なので、タブやウェブコンテンツのウィンドウを扱うときは覚えておいてください。load イベントハンドラを gBrowser に取り付けると、タブがいくつ開いていても、すべてのタブのイベントをリッスンできるようになります。gBrowser は、すべてのブラウザウィンドウに存在します。ここでは、あとで必要なくなった時に削除するため、このハンドラ関数をプライベート変数に格納します。

gBrowser.removeEventListener("load", this._loadHandler, true);

最後に、実際のとても簡単なハンドラのコードです:

_onPageLoad : function(event) {
  // これは読み込んだページのコンテンツドキュメントです。
  let doc = event.originalTarget;

  if (doc instanceof HTMLDocument) {
    // ページ内部のフレームか?
    if (doc.defaultView.frameElement) {
      // タブ内のフレームが読み込まれた。
      // ルートドキュメントを探す:
      while (doc.defaultView.frameElement) {
        doc = doc.defaultView.frameElement.ownerDocument;
      }
    }
  }
}

2 番目の if 分岐は、ページ内部のフレームに読み込まれた HTML ドキュメントと区別できるようにするために必要です。ごく少数のサイトでフレームセットが使用されていますが、一般的には、iframe 要素内に広告を表示するために使用されています。多くの場合、ページの URL をいくつかの文字列や正規表現で比較する必要があるでしょう:

if (SOME_REGULAR_EXPRESSION.test(doc.defaultView.location.href))

XUL や HTML ドキュメントに対して行うように、読み込んだページの DOM へアクセスしたり変更したりできます。

しかしながら、ページの読み込みを簡単にキャンセルすることはできません。タブを閉じるか、about:blank ページや他のページへリダイレクト、またはブラウザにこのページの読み込みを中止させることならできます。しかし一般的には、この動作がユーザにはバグのように見えるため、このようなことは行いたくないでしょう。コンテンツがダウンロードされ、タブに何か表示される前にページの読み込みに割り込む良い方法があります。

HTTP オブザーバ

もう 1 つの読み込みを検出して割り込む一般的な方法は、HTTP オブザーバのトピックを使用することです。これは、拡張機能の Tamper Data などが行っている方法です。

HTTP 通知は、Firefox を起源とするすべての HTTP 要求に対して発生します。これらはウィンドウから独立しているため、オブザーバコードを非 chrome オブジェクト内に記述しておくとよいでしょう。また、ウィンドウが 2 つ以上開いている場合は、重複した作業を避けるようにしなければなりません。

Observer Notifications のページに定義されたリッスンできる HTTP トピックは 2 つあります:

トピック 説明
http-on-modify-request HTTP 要求が行われたときに呼ばれます。ハンドラなどの変更を許可するチャンネルが利用可能です。
http-on-examine-response 応答が受け取られた後にウェブサーバから呼ばれます。ハンドラはチャンネル上で利用可能です。

observe メソッドの subject 引数は、トピックに付随する開かれた、またはすでに開いている HTTP チャンネルに相当する nsIChannel オブジェクトです。

observe : function(aSubject, aTopic, aData) {
  if (TOPIC_MODIFY_REQUEST == aTopic) {
    let url;

    aSubject.QueryInterface(Ci.nsIHttpChannel);
    url = aSubject.URI.spec;

    if (RE_URL_TO_MODIFY.test(url)) { // RE_URL_TO_MODIFY は正規表現です
      aSubject.setRequestHeader("Referer", "http://example.com", false);
    } else if (RE_URL_TO_CANCEL.test(url)) { // RE_URL_TO_MODIFY は正規表現です
      aSubject.cancel(Components.results.NS_BINDING_SUCCEEDED);
    }
  }
}

この例は、要求のための URL を取得し、それを正規表現を使用して解析し、HTTP ヘッダの編集などを実行する方法、またはその要求をキャンセルする方法を示しています。MDC のページは説明が欠けています。メソッドと属性についての詳細は、古い XUL Planet のページをお読みください。

HTTP オブザーバを追加する時は、効率がとても重要です。あなたのオブザーバメソッドが Firefox によるすべての HTTP 要求に対して呼び出されることを忘れないでください。大抵は、ページを訪れるごとに呼び出されます。前の例で最初に行ったことの一つは、URL が私たちの求めるものかどうか確認し、そうでなければ、そのままページを開くことでした。重く、時間のかかる操作は避けてください。さもなければ、ユーザをひどく苛立たせることになるでしょう。

HTTP オブザーバは、読み込みの検出と URL によるフィルタリングをするには十分です。ただし、特にリダイレクトが発生した時、ページの読み込みがいくつかの HTTP 要求を起こすことに対処しなければなりません。ブラウザに gmail.com と入力すると、実際に何らかのコンテンツが表示されるページへたどり着くまでに数回のリダイレクトが行われ、これらすべての "ホップ" があなたのオブザーバを呼び出します。

一方で、HTTP オブザーバは、応答コンテンツを読んだり操作したりすることが苦手です。Tamper Data が行っているようなことを試してみてください。キャッシュデータを Cache サービス から展開し、チャンネルが nsICachingChannel を実装していたらキャッシュトークンを使用します。これは複雑で非同期であり、とても信頼できるものではありません。load イベントで行うように、DOM に変更を加えることはできないでしょう。コンテンツを変更する必要がある場合は、HTTP オブザーバを使用する方法は相応しくありません。

WebProgressListener

chrome 内で使用される場合、これは、ページ読み込み時の様々なステージに割り込んだり変更したりするためのより洗練された方法です。しかし、この方法でも常に支払うべき代価があります: chrome 内の WebProgressListener は、browser 要素の特定のインスタンスに取り付けられます。これは何を意味するのでしょうか? これは、あなたのリスナーを追加または削除するために、タブの開閉を追跡しなければならないことを意味します。次のコードのサンプルは、すべてのタブに対するプログレスリスナーを追跡します:

init : function() {
  gBrowser.browsers.forEach(function (browser) {
    this._toggleProgressListener(browser.webProgress, true);
  }, this);

  gBrowser.tabContainer.addEventListener("TabOpen", this, false);
  gBrowser.tabContainer.addEventListener("TabClose", this, false);
},

uninit : function() {
  gBrowser.browsers.forEach(function (browser) {
    this ._toggleProgressListener(browser.webProgress, false);
  }, this);

  gBrowser.tabContainer.removeEventListener("TabOpen", this, false);
  gBrowser.tabContainer.removeEventListener("TabClose", this, false);
},

handleEvent : function(aEvent) {
  let tab = aEvent.target;
  let webProgress = gBrowser.getBrowserForTab(tab).webProgress;

  this._toggleProgressListener(webProgress, ("TabOpen" == aEvent.type));
},

_toggleProgressListener : function(aWebProgress, aIsAdd) {
  if (aIsAdd) {
    aWebProgress.addProgressListener(this, aWebProgress.NOTIFY_ALL);
  } else {
    aWebProgress.removeProgressListener(this);
  }
}

このコードは、それほど難しくないでしょう。最初のタブに対して手動でプログレスリスナーの登録と登録解除をし、すべてのタブに対する残りのリスナーを追跡できるようにするため、TabOpen と TabClose イベントリスナーを追加しています。すべてのリスナーの削除については、メモリリークの原因となる可能性を無くすように注意を払っています。

プログレスリスナーのメソッドの実装と NOTIFY_ALL についての説明がまだ残っています。これらについては、はじめに WebProgressListenersWebProgress NOTIFY 定数のドキュメントを読むことをお勧めします。簡単に説明すると、プログレスリスナーには数多くの状態遷移フラグがあり、ページ読み込み時にそれらの状態が変更され、NOTIFY 変数でリッスンする必要のないイベントを除外できます。フィルタを正しく選ぶことは、コードをシンプルにするだけでなく、あなたの拡張機能による通常のページ移動時のパフォーマンスへの影響を減らします。

以下は、2 つの一般的な使用例と WebProgressListener でこれらを実装する方法です:

  • ページの読み込みイベントのときのように、簡単な検出とフィルタリングを行いたい場合は、onLocationChange が使用できます。aLocation.spec を使用して URL を取得し、正規表現に対してこれをマッチさせてください。要求オブジェクトの aRequest が処理中の要求を保持します。aRequest.cancel(NS_BINDING_ABORTED) を実行すると、これをキャンセルできます。aWebProgress.DOMWindow がコンテンツが読み込まれるウィンドウへのアクセスを提供します。
  • 時々、リダイレクトのことを気にせずに、最後に読み込まれ、実際のコンテンツを保持するページだけを検出したいことがあるでしょう。この場合の最善策は、onStateChange を使用し、状態遷移フラグがドキュメントが読み込まれ始めたことを示した時にフィルタをかけることです:
if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
    (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT))

バイナリマスクの & 演算子が使用されていることに注意してください。

これがフレームに読み込まれているかどうか検出するには、次のようにします:

if (aWebProgress.DOMWindow != aWebProgress.DOMWindow.top) {
  // これはフレームです
}

この場合、URL は aRequest.name から取得できます。このプロパティは、状態を調べる if 条件のブロック内からアクセスするようにしてください。他の場所からこのプロパティにアクセスすると、例外が投げられる可能性があります。要求をキャンセルすると、onLocationChange と同じように動作します。

XPCOM による解決策

前述の方法が適用できないときは、残りの 2 つの解決策を試してみてください。これ等の方法は、既存の Firefox インタフェースを実装する XPCOM コンポーネントを作成する必要があります。これらは、あなたのアプリケーションの仕組みがほとんど XPCOM で実現されている場合や、読み込み処理中のただ一点だけを調査する必要がある場合にとても役立つでしょう。多くの場合は前に説明した解決策で充分なので、これらは簡単に説明します。

Document Loader サービス

Document Loader サービス は、WebProgressListener 以外の何ものでもありません。nsIWebProgressListener を拡張する XPCOM コンポーネントを作成し、これを含めるためにサービス内で addProgressListener メソッドを使用してください。前に言及したことのすべてが、ここでも同様に適用されます。ただし、すべてのタブとウィンドウのイベントを 1 個のオブジェクトで受け取るため、タブの開閉時に毎回リスナーを追加したり削除したりすることについては心配しないでください。

この方法はまた、ブラウザウィンドウ内だけでなく、アプリケーション内のどこでもページの読み込みを検出できる長所があります。

ウェブフィルタリングのための拡張機能を作成する場合は、(限定的な) ウェブナビゲーションを提供する DOM Inspector ウィンドウやアドオンマネージャウィンドウなどの XUL ウィンドウのことを心に留めておかなければなりません。他の拡張機能がウェブナビゲーションを提供する XUL ウィンドウを追加する場合も同様です。このような場合は、この XPCOM を使用したグローバルな解決策を用いるのが最善です。

コンテンツポリシー

最後に、nsIContentPolicy を実装する選択肢があります。nsIContentPolicy を拡張する XPCOM コンポーネントを作成し、これを Category Manager を使用して "content-policy" に登録してください。

ここで本当に役立つただ一つのインタフェースは、shouldLoad メソッドだけです。これは、コンテンツの URI を引数として直接取得し、コンテンツが読み込まれたかどうかを明確な値の戻り値で示すことができるため、前に見てきたほとんどの解決策よりもスマートなコードになるでしょう。context パラメータは、コンテンツを読み込むウィンドウへのアクセスを提供します。

他のすべての解決策のように、開始時から不要なケースを除外して、効率的で早道な方法でこれを行う必要があります。shouldLoad は、Firefox が画像やスクリプト、XUL ドキュメントを読み込む時に毎回呼び出されます。よいフィルタは次のようになります:

shouldLoad : function(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
  let result = Ci.nsIContentPolicy.ACCEPT;

  // フレームに対して行ったのと同様に TYPE_SUBDOCUMENT を確認します
  if ((Ci.nsIContentPolicy.TYPE_DOCUMENT == aContentType) &&
      SOME_REGULAR_EXPRESSION.test(aContentLocation.spec)) {
    // result を変更する何らかのコード
  }

  return result;
}

コンテンツポリシーは、処理の初期段階 (要求が出される前) に適用されるため、とてもクリーンなキャンセル操作を行えます。この特徴は、このアプローチに対して 2 つの制限をもたらします。1 つ目は、読み込まれるコンテンツを簡単に読んだり変更したりできないことです。2 つ目は、shouldLoad メソッドがリダイレクトに対しては呼び出されないことです。最初の URL 要求に対して 1 回だけ呼び出されます。これをやり過ごす場合は、注意を払わずにどこへでもリダイレクトできます。このアプローチの仕方は、AdBlock Plus などの有名なフィルタリング拡張で用いられています。とは言え、他の解決策を先に検討することをお勧めします。あなたの拡張機能の必要に応じて、他の解決策を組み合わせることになるでしょう。

This tutorial was kindly donated to Mozilla by Appcoast.

ドキュメントのタグと貢献者

 このページの貢献者: chrisdavidmills, ethertank, Marsf
 最終更新者: chrisdavidmills,