コンテンツスクリプトは、特定のWebページのコンテキストで実行される拡張機能の一部です(拡張機能の一部であるバックグラウンドスクリプトや、Webサイト自体の一部であるスクリプト、例えば <script> 要素みたいなものと対をなすような)。

バックグラウンドスクリプトはすべての WebExtension JavaScript API にアクセスできますが、Webページのコンテンツに直接アクセスすることはできません。だからあなたの拡張機能がそれを行う必要がある場合は、コンテンツスクリプトが必要です。

通常のWebページで読み込まれたスクリプトと同様に、コンテンツスクリプトは、標準の DOM API を使用してページのコンテンツを読み取り、変更することができます。

コンテンツスクリプトは、 WebExtension API の小さなサブセット にしかアクセスできませんが、メッセージングシステムを使用して バックグラウンドスクリプトと通信し、WebExtension API に間接的にアクセスすることができます。

コンテンツスクリプトは現在 addons.mozilla.org と testpilot.firefox.com でブロックされています。このドメインのページにコンテンツスクリプトを挿入しようとすると、そのスクリプトは失敗し、ページは CSP エラーをログに記録します。

コンテンツスクリプトの読み込み

次の3つの方法のいずれかを使用して、Webページにコンテンツスクリプトを読み込むことができます。

  1. インストール時に、URLパターンにマッチするページ内へ: manifest.json の content_scripts キーを使用して、URLが指定されたパターンにマッチするページをロードするたびにコンテンツスクリプトを読み込むようブラウザーに依頼できます。
  2. 実行時に、URLパターンにマッチするページ内へ: contentScripts API を使って、URLが指定されたパターンにマッチするページをロードするたびにコンテンツスクリプトを読み込むようブラウザーに依頼できます。これは method (1) のようなもので、違いは実行時にコンテンツスクリプトを追加/削除できることです。
  3. 実行時に、特定のタブへ: tabs.executeScript() APIを使用すると、ユーザーがブラウザーアクションをクリックした場合など、必要なときにコンテンツスクリプトを特定のタブに読み込むことができます。

拡張機能のフレームごとにグローバルスコープが1つしかないため、コンテンツスクリプトの読み込み方法に関係なく、1つのコンテンツスクリプトの変数に別のコンテンツスクリプトが直接アクセスできます。

コンテンツスクリプト環境

DOM アクセス

コンテンツスクリプトは、普通のページスクリプトと同様に、ページの DOM にアクセスして修正できます。ページスクリプトにてなされたDOMの変更を見ることもできます。

しかし、コンテンツスクリプトは "DOMのきれいな見た目" を取得します。これはつまり:

  • コンテンツスクリプトはページスクリプトにて定義された  JavaScript 変数を見ることができない
  • ページスクリプトが組み込み DOM プロパティを再定義した場合、コンテンツスクリプトはそのプロパティの(再定義後でなく)オリジナル値を見ている

Geckoでは、この挙動は Xray vision と呼ばれます。

例えば、次のウェブページを考えます:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  </head>

  <body>
    <script src="page-scripts/page-script.js"></script>
  </body>
</html>

"page-script.js" スクリプトは次を実行します:

// page-script.js

// add a new element to the DOM
var p = document.createElement("p");
p.textContent = "This paragraph was added by a page script.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// define a new property on the window
window.foo = "This global variable was added by a page script";

// redefine the built-in window.confirm() function
window.confirm = function() {
  alert("The page script has also redefined 'confirm'");
}

今度は拡張機能がページにコンテンツスクリプトを挿入します:

// content-script.js

// can access and modify the DOM
var pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";

// can't see page-script-added properties
console.log(window.foo);  // undefined

// sees the original form of redefined properties
window.confirm("Are you sure?"); // calls the original window.confirm()

その反対もまた真です: ページスクリプトはコンテンツスクリプトが追加した JavaScript プロパティを見れません。

このことからすると、コンテンツスクリプトは予言できる振る舞いをする DOM プロパティに依存していて、ページスクリプトにて定義された変数とコンテンツスクリプトで定義する変数の衝突は心配しなくていいです。

この振る舞いの実用的な結論として、コンテンツスクリプトはページに読み込まれたいかなる JavaScript ライブラリにもアクセスしません。なので例えば、ページに jQuery が含まれても、コンテンツスクリプトは見ることができません。

コンテンツスクリプトから JavaScript ライブラリを使いたい場合、ライブラリ自身を、使う方のコンテンツスクリプトと並べて挿入するべきです:

"content_scripts": [
  {
    "matches": ["*://*.mozilla.org/*"],
    "js": ["jquery.js", "content-script.js"]
  }
]

Note that Firefox provides some APIs that enable content scripts to access JavaScript objects created by page scripts and to expose their own JavaScript objects to page scripts. See Sharing objects with page scripts for more details.

WebExtension API

標準 DOM API に加え、コンテンツスクリプトは次の WebExtension API を使用できます:

extensionから:

runtimeから:

i18nから:

storageの全て。

XHR と Fetch

コンテンツスクリプトは通常の window.XMLHttpRequestwindow.fetch() API を使ってリクエストを作成できます。

コンテンツスクリプトは拡張機能の他の部分と同一のクロスドメイン権限を取得します: よって拡張機能が manifest.json の permissions キーを使ってあるドメインのクロスドメインアクセスを要求している場合、コンテンツスクリプトも同様にそのドメインのアクセスを取得します。

これはより多く権限付けられた XHR に晒して、コンテンツスクリプトでインスタンスを取得することで達成し、その副作用としてページ自体からのリクエストがそうであるように OriginReferer ヘッダーがセットされず、リクエストからクロスオリジンな性質を隠すことが好ましいことがよくあります。バージョン 58 以降、コンテンツ自体から送られたかのようなリクエストを必要とする拡張機能は  content.XMLHttpRequestcontent.fetch() を代わりに使うことができます。クロスブラウザー拡張機能にとってこれらの存在は機能検出となります。

バックグラウンドスクリプトとの通信

コンテンツスクリプトは WebExtension API の大半を直接には使用できませんが、メッセージAPI を用いて拡張機能のバックグラウンドスクリプトと通信できて、それゆえにバックグラウンドスクリプトがアクセスできる全ての API に間接的にアクセスできます。

バックグラウンドスクリプトとコンテンツスクリプトが通信する2つのパターンがあります: レスポンスがオプションなワンオフメッセージを送るのと、お互いに息の長いコネクションを確立して、そこでメッセージを交換するのとです。

ワンオフメッセージ

レスポンスがオプションなワンオフメッセージを送るには、次の API を使います:

  コンテンツスクリプト内 バックグラウンドスクリプト内
送信メッセージ browser.runtime.sendMessage() browser.tabs.sendMessage()
受信メッセージ browser.runtime.onMessage browser.runtime.onMessage

例えば、ウェブページでのクリックイベントをリッスンするコンテンツスクリプトがここにあります。クリックがリンク上である場合、ターゲットURLをバックグラウンドページにメッセージします。

// content-script.js

window.addEventListener("click", notifyExtension);

function notifyExtension(e) {
  if (e.target.tagName != "A") {
    return;
  }
  browser.runtime.sendMessage({"url": e.target.href});
}

バックグラウンドスクリプトはこのメッセージをリッスンして、 notifications API を使って通知を表示します:

// background-script.js

browser.runtime.onMessage.addListener(notify);

function notify(message) {
  browser.notifications.create({
    "type": "basic",
    "iconUrl": browser.extension.getURL("link.png"),
    "title": "You clicked a link!",
    "message": message.url
  });
}

この例のコードは Github の notify-link-clicks-i18n のサンプルから簡単に適用できます。

コネクションベースのメッセージ

ワンオフメッセージの送信は、バックグラウンドスクリプトとコンテンツスクリプトとで多くのメッセージを交換する場合はめんどくさいです。なので代替パターンは、この2つのコンテキスト間で寿命の長いコネクションを確立して、メッセージ交換にこのコネクションを使うことです。

いずれの側にも runtime.Port オブジェクトがあり、メッセージ交換に使えます。

コネクションを作成するには:

それぞれがポートを持ったら、両方が runtime.Port.postMessage() でメッセージを送って runtime.Port.onMessage でメッセージを受信できるようになる。

例えば、ロードしたらすぐに、このコンテンツスクリプトは:

  • バックグラウンドに接続し、myPort 変数に Port を格納する
  • myPort のメッセージをリッスンしてログに出す
  • ユーザーがドキュメントをクリックしたとき、バックグラウンドスクリプトに myPort を使ってメッセージを送る
// content-script.js

var myPort = browser.runtime.connect({name:"port-from-cs"});
myPort.postMessage({greeting: "hello from content script"});

myPort.onMessage.addListener(function(m) {
  console.log("In content script, received message from background script: ");
  console.log(m.greeting);
});

document.body.addEventListener("click", function() {
  myPort.postMessage({greeting: "they clicked the page!"});
});

対応するバックグラウンドスクリプトは:

  • コンテンツスクリプトからの通信試行をリッスンする
  • 通信試行を受け取ったとき:
    • portFromCS という名前の変数にポートを格納する
    • そのポートを使ってコンテンツスクリプトにメッセージを送る
    • ポートに届いたメッセージをリッスンしてログに出す
  • ユーザーが拡張機能のブラウザーアクションをクリックしたとき、portFromCS を使ってコンテンツスクリプトにメッセージを送る
// background-script.js

var portFromCS;

function connected(p) {
  portFromCS = p;
  portFromCS.postMessage({greeting: "hi there content script!"});
  portFromCS.onMessage.addListener(function(m) {
    console.log("In background script, received message from content script")
    console.log(m.greeting);
  });
}

browser.runtime.onConnect.addListener(connected);

browser.browserAction.onClicked.addListener(function() {
  portFromCS.postMessage({greeting: "they clicked the button!"});
});

複数のコンテンツスクリプト

同時に複数のコンテンツスクリプトが通信する場合、各接続を配列に格納するのが良いかもしれません。

 

// background-script.js

var ports = []

function connected(p) {
  ports[p.sender.tab.id]    = p
  //...
}

browser.runtime.onConnect.addListener(connected)

browser.browserAction.onClicked.addListener(function() {
  ports.forEach(p => {
        p.postMessage({greeting: "they clicked the button!"})
    })
});

 

ウェブページとの通信

コンテンツスクリプトは既定ではページスクリプトが作成したオブジェクトにアクセスできませんが、 DOM window.postMessagewindow.addEventListener API を使ってページスクリプトと通信できます。

例えば:

// page-script.js

var messenger = document.getElementById("from-page-script");

messenger.addEventListener("click", messageContentScript);

function messageContentScript() {
  window.postMessage({
    direction: "from-page-script",
    message: "Message from the page"
  }, "*");
// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data &&
      event.data.direction == "from-page-script") {
    alert("Content script received message: \"" + event.data.message + "\"");
  }
});

これの完全な動作サンプルは、GitHubのデモページを訪れて指示に従ってください。

この方法ではいつでも信頼できないウェブコンテンツと相互作用し、細心の注意が必要です。拡張機能は強力な力を持つコードの権限があり、敵意のあるウェブページは簡単にこの力にアクセスします。

細かい例を作るには、メッセージを受け取ったコンテンツスクリプトがこのようなことを行うと仮定してください:

// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data.direction &&
      event.data.direction == "from-page-script") {
    eval(event.data.message);
  }
});

今やページスクリプトはコンテンツスクリプトの全ての権限でコードを実行できます。

コンテンツスクリプト内で eval() を使う

Chrome では、 eval() は常にページコンテキストではなくてコンテンツスクリプトのコンテキストで動作します。

Firefox では:

  • eval() を呼ぶ場合、コンテンツスクリプトのコンテキストで動作します。
  • window.eval() を呼ぶ場合、ページのコンテキストで動作します。

例えば、こんなコンテンツスクリプトを考えてみます:

// content-script.js

window.eval('window.x = 1;');
eval('window.y = 2');

console.log(`In content script, window.x: ${window.x}`);
console.log(`In content script, window.y: ${window.y}`);

window.postMessage({
  message: "check"
}, "*");

このコードは単に変数 x と y を、window.eval()eval() を用いて作成し、値をログに出し、ページにメッセージします。

メッセージの受信に際し、ページスクリプトは同じ変数をログに出します:

window.addEventListener("message", function(event) {
  if (event.source === window && event.data && event.data.message === "check") {
    console.log(`In page script, window.x: ${window.x}`);
    console.log(`In page script, window.y: ${window.y}`);
  }
});

Chrome では、こんな出力が生成されます:

In content script, window.x: 1
In content script, window.y: 2
In page script, window.x: undefined
In page script, window.y: undefined

Firefox では、こんな出力が生成されます:

In content script, window.x: undefined
In content script, window.y: 2
In page script, window.x: 1
In page script, window.y: undefined

同じことは setTimeout()setInterval()Function() にも言えます。

ページのコンテキストでコードを実行するときは注意してください。ページの環境変数が悪意をはらんだウェブページにコントロールされ、未知の方法であなたが操作するオブジェクトを再定義するかもしれません。

// page.js redefines console.log

var original = console.log;

console.log = function() {
  original(true);
}
// content-script.js calls the redefined version

window.eval('console.log(false)');

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

タグ: 
 このページの貢献者: Uemmra3, mfuji09, m0a02
 最終更新者: Uemmra3,