コンテンツスクリプト
コンテンツスクリプトは、特定のウェブページのコンテキストで実行される拡張機能の一部です(拡張機能の一部であるバックグラウンドスクリプトや、ウェブサイト自体の一部であるスクリプト、例えば <script>
要素みたいなものと対をなすような)。
バックグラウンドスクリプトはすべての WebExtension JavaScript API にアクセスできますが、ウェブページのコンテンツに直接アクセスすることはできません。だからあなたの拡張機能がそれを行う必要がある場合は、コンテンツスクリプトが必要です。
通常のウェブページで読み込まれたスクリプトと同様に、コンテンツスクリプトは、標準の DOM API を使用してページのコンテンツを読み取り、変更することができます。
コンテンツスクリプトは、WebExtension API の小さなサブセット にしかアクセスできませんが、メッセージングシステムを使用して バックグラウンドスクリプトと通信し、WebExtension API に間接的にアクセスすることができます。
コンテンツスクリプトは次のドメインでブロックされるのに注意してください:
- accounts-static.cdn.mozilla.net
- accounts.firefox.com
- addons.cdn.mozilla.net
- addons.mozilla.org
- api.accounts.firefox.com
- content.cdn.mozilla.net
- content.cdn.mozilla.net
- discovery.addons.mozilla.org
- input.mozilla.org
- install.mozilla.org
- oauth.accounts.firefox.com
- profile.accounts.firefox.com
- support.mozilla.org
- sync.services.mozilla.com
- testpilot.firefox.com
このドメインのページにコンテンツスクリプトを挿入しようとすると、そのスクリプトは失敗し、ページは CSP エラーをログに記録します。
addons.mozilla.org を含む制限のために、ユーザーはインストール後すぐに拡張機能を試して、動かないのに気付くだけでしょう! 適切な警告を追加するか、addons.mozilla.org
から動かす onboarding page を追加したくなるでしょう。
letfoo
や window.foo = "bar"
にて、コンテンツスクリプトのグローバルスコープで追加された値は、1408996 のバグによって消えることがあります。
コンテンツスクリプトの読み込み
次の 3 つの方法のいずれかを使用して、ウェブページにコンテンツスクリプトを読み込むことができます。
-
- インストール時に、URL パターンにマッチするページ内へ
manifest.json
のcontent_scripts
キーを使用して、URL が指定されたパターンにマッチするページをロードするたびにコンテンツスクリプトを読み込むようブラウザーに依頼できます。
-
- 実行時に、URL パターンにマッチするページ内へ
contentScripts
API を使って、URL が指定されたパターンにマッチするページをロードするたびにコンテンツスクリプトを読み込むようブラウザーに依頼できます。これは method (1) のようなもので、違いは実行時にコンテンツスクリプトを追加/削除できることです。
-
- 実行時に、特定のタブへ
tabs.executeScript()
API を使用すると、ユーザーがブラウザーアクションをクリックした場合など、必要なときにコンテンツスクリプトを特定のタブに読み込むことができます。
フレームごと、拡張機能ごとのグローバルスコープしかありません。これは 1 つのコンテンツスクリプトの変数は、読み込み方にかかわらず、他のコンテンツスクリプトからアクセスできることになります。
方法 (1) と (2) ではマッチパターンを使って表現された URL のスクリプトだけを読み込みできます。
方法 (3) では、拡張機能と一緒にパッケージされたページのスクリプトも読み込みできますが、"about:debugging" や "about:addons"のような権限つきページにはスクリプトを読み込めません。
コンテンツスクリプト環境
DOM アクセス
コンテンツスクリプトは、普通のページスクリプトと同様に、ページの DOM にアクセスして修正できます。ページスクリプトにてなされた DOM の変更を見ることもできます。
しかし、コンテンツスクリプトは "DOM のきれいな見た目" を取得します。これはつまり:
- コンテンツスクリプトはページスクリプトにて定義された JavaScript 変数を見ることができない
- ページスクリプトが組み込み DOM プロパティを再定義した場合、コンテンツスクリプトはそのプロパティの(再定義後でなく)オリジナル値を見ている
Firefox では、この挙動は Xray vision (en-US) と呼ばれます。
例えば、次のウェブページを考えます:
<!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
let 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
let 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"]
}
]
記: Firefox ではコンテンツスクリプトからページスクリプトによって生成された JavaScript オブジェクトにアクセスしたり、ページスクリプトにコンテンツスクリプトの JavaScript オブジェクトを公開できるようにする API が提供されます。
詳しくはページスクリプトとオブジェクトを共有する (en-US)のページを見てください。
WebExtension API
XHR と Fetch
コンテンツスクリプトは通常の window.XMLHttpRequest
と window.fetch()
API を使ってリクエストを作成できます。
Firefox では、コンテンツスクリプトの (例えば、fetch()
を使った) リクエストは、拡張機能のコンテキストで起こるので、ページコンテンツを参照する URL を絶対URL で提供せねばなりません。
Chrome では、リクエストはページのコンテ キストで起こるので、相対 URL で行われます。例えば、/api
は https://[現在のペー ジの URL]/api
に送られます。
コンテンツスクリプトは拡張機能の他の部分と同一のクロスドメイン権限を取得します: よって拡張機能が manifest.json
の permissions
キーを使ってあるドメインのクロスドメインアクセスを要求している場合、コンテンツスクリプトも同様にそのドメインのアクセスを取得します。
これはより多く権限付けられた XHR に晒して、コンテンツスクリプトでインスタンスを取得することで達成し、その副作用としてページ自体からのリクエストがそうであるように Origin
と Referer
ヘッダーがセットされず、リクエストからクロスオリジンな性質を隠すことが好ましいことがよくあります。
バージョン 58 以降、コンテンツ自体から送られたかのようなリクエストを必要とする拡張機能は content.XMLHttpRequest
と content.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 (en-US)
オブジェクトがあり、メッセージ交換に使えます。
コネクションを作成するには:
- 片方で
runtime.onConnect (en-US)
にてコネクションをリッスンする。 - もう片方で次を呼び出す:
tabs.connect() (en-US)
(コンテンツスクリプトに接続する場合)runtime.connect() (en-US)
(バックグラウンドスクリプトに接続する場合)
これは runtime.Port (en-US)
オブジェクトを返します。
runtime.onConnect (en-US)
リスナーには自身のruntime.Port (en-US)
オブジェクトが渡される。
それぞれがポートを持ったら、両方が:
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!"})
})
});
ワンオフメッセージとコネクションベースのメッセージとの選択
ワンオフとコネクションベースのメッセージの選択は、拡張機能がどうメッセージを利用すると期待されるかに依存します。
推奨のベストプラクティスは次の通りです:
次のときにワンオフメッセージを使用…
- メッセージに 1 つだけの応答がある場合
- メッセージの受信を少しのスクリプトがリッスンする場合(
runtime.onMessage
呼び出し)
次のときにコネクションベースメッセージを使用…
- スクリプトが、複数のメッセージを交換するセッションに関わる場合
- 拡張機能がタスクの進捗や、タスクが中断されたのを知る必要がある場合、または初期化されたタスクをメッセージング経由で中断したい場合
ウェブページとの通信
既定では、コンテンツスクリプトはページスクリプトが作成したオブジェクトにアクセスできませんが、DOM window.postMessage
と window.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()
を呼ぶ場合、ページのコンテキストで動作します。, it runs code in the context of the content script.
例えば、こんなコンテンツスクリプトを考えてみます:
// 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)');