コンテンツ スクリプトは拡張機能ではなくウェブページのコンテキストで実行されるため、拡張機能の他の部分と通信する手段が必要になることがよくあります。たとえば、RSS リーダー拡張機能は、コンテンツ スクリプトを使用してページに RSS フィードが存在するかどうかを検出し、バックグラウンド ページに通知して、そのページのページ アクション アイコンを表示します。
拡張機能とそのコンテンツ スクリプト間の通信は、メッセージ パッシングを使用して行われます。どちら側も、相手側から送信されたメッセージをリッスンし、同じチャネルで応答できます。メッセージには、有効な JSON オブジェクト(null、ブール値、数値、文字列、配列、オブジェクト)を含めることができます。1 回限りのリクエスト用のシンプルな API と、共有コンテキストで複数のメッセージを交換するための長時間 接続を可能にする複雑な API があります。ID がわかっている場合は、別の拡張機能にメッセージを送信することも可能です。これについては、拡張機能間の メッセージのセクションで説明します。
シンプルな 1 回限りのリクエスト
拡張機能の別の部分に 1 つのメッセージを送信するだけでよい場合(必要に応じて応答を受け取る)、簡略化された runtime.sendMessage または tabs.sendMessage を使用する必要があります。これにより、コンテンツ スクリプトから拡張機能に、またはその逆方向に、JSON シリアル化可能な 1 回限りのメッセージを送信できます。必要に応じてコールバック パラメータを使用して、相手側からの応答を処理できます。
コンテンツ スクリプトからリクエストを送信する例を次に示します。
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
拡張機能からコンテンツ スクリプトにリクエストを送信する場合も、送信先のタブを指定する必要がある点を除いて、ほぼ同じです。この例では、選択したタブのコンテンツ スクリプトにメッセージを送信しています。
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
console.log(response.farewell);
});
});
受信側では、メッセージを処理するために runtime.onMessage イベント リスナーを設定する必要があります。これは、コンテンツ スクリプトまたは拡張機能ページから見ても同じです。
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
}
);
上記の例では、sendResponse が同期的に呼び出されました。sendResponse を非同期で使用する場合は、onMessage イベント ハンドラに return true; を追加します。
sendResponse コールバックは、同期的に使用する場合、またはイベント ハンドラが非同期で応答することを示す true を返す場合にのみ有効です。ハンドラが true を返さない場合、または sendResponse コールバックがガベージ コレクションされると、sendMessage 関数のコールバックが自動的に呼び出されます。長時間接続
1 回のリクエストとレスポンスよりも長い会話が必要になることがあります。 tabs.connectチャネルには名前を付けることもでき、さまざまな種類の接続を区別できます。
ユースケースの 1 つとして、フォームの自動入力拡張機能があります。コンテンツ スクリプトは、特定のログイン用のチャネルを拡張機能ページに開き、ページの各入力要素に対して拡張機能にメッセージを送信して、入力するフォームデータをリクエストします。共有接続により、拡張機能はコンテンツ スクリプトから送信された複数のメッセージをリンクする共有状態を維持できます。
接続を確立すると、各エンドに runtime.Port オブジェクトが与えられます。このオブジェクトは、その接続を介してメッセージを送受信するために使用されます。
コンテンツ スクリプトからチャネルを開き、メッセージを送信してリッスンする方法は次のとおりです。
var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
if (msg.question == "Who's there?")
port.postMessage({answer: "Madame"});
else if (msg.question == "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
});
拡張機能からコンテンツ スクリプトにリクエストを送信する場合も、接続先のタブを指定する必要がある点を除いて、ほぼ同じです。上記の例の connect の呼び出しを tabs.connectに置き換えるだけです。
受信接続を処理するには、runtime.onConnect イベント リスナーを設定する必要があります。これは、コンテンツ スクリプトまたは拡張機能ページから見ても同じです。拡張機能の別の部分が「connect()」を呼び出すと、このイベントが発生し、接続を介してメッセージを送受信するために使用できる runtime.Port オブジェクトが渡されます。受信接続に応答する例を次に示します。
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name == "knockknock");
port.onMessage.addListener(function(msg) {
if (msg.joke == "Knock knock")
port.postMessage({question: "Who's there?"});
else if (msg.answer == "Madame")
port.postMessage({question: "Madame who?"});
else if (msg.answer == "Madame... Bovary")
port.postMessage({question: "I don't get it."});
});
});
ポートの有効期間
ポートは、拡張機能のさまざまな部分間の双方向通信方法として設計されています。ここで、フレーム(最上位)が最小の部分と見なされます。 tabs.connect、runtime.connect、runtime.connectNative を呼び出すと、ポート が作成されます。このポートは、 postMessage を介して相手側にメッセージを送信するためにすぐに使用できます。
タブに複数のフレームがある場合、tabs.connect を呼び出すと、 runtime.onConnect イベントが複数回呼び出されます(タブ内のフレームごとに 1 回)。同様に、 runtime.connect を使用すると、onConnect イベントが複数回発生する可能性があります(拡張機能プロセスの フレームごとに 1 回)。
たとえば、開いているポートごとに個別の状態を維持している場合は、接続が閉じられたタイミングを確認する必要があります。これには、runtime.Port.onDisconnect イベントをリッスンします。このイベントは、チャネルの相手側に有効なポートがない場合に発生します。これは、次のような場合に発生します。
- 相手側に runtime.onConnect のリスナーがない。
- ポートを含むタブがアンロードされた(タブが移動した場合など)。
connectが呼び出されたフレームがアンロードされた。- ポートを受け取ったすべてのフレーム(runtime.onConnect 経由)がアンロードされた。
- runtime.Port.disconnect が 相手側から呼び出された。
connect呼び出しの結果、受信側のポートが複数になり、これらのポートのいずれかでdisconnect()が呼び出された場合、onDisconnectイベントは送信側のポートでのみ発生し、他のポートでは発生しません。
拡張機能間のメッセージ
拡張機能内のさまざまなコンポーネント間でメッセージを送信するだけでなく、Messaging API を使用して他の拡張機能と通信することもできます。これにより、他の拡張機能が利用できる公開 API を公開できます。
受信リクエストと接続をリッスンすることは、 runtime.onMessageExternal メソッドまたは runtime.onConnectExternal メソッドを使用する点を除いて、内部の場合と似ています。それぞれの例を次に示します。
// For simple requests:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id == blocklistedExtension)
return; // don't allow this extension access
else if (request.getTargetData)
sendResponse({targetData: targetData});
else if (request.activateLasers) {
var success = activateLasers();
sendResponse({activateLasers: success});
}
});
// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
同様に、別の拡張機能にメッセージを送信することも、拡張機能内でメッセージを送信する場合と似ています。 唯一の違いは、通信する拡張機能の ID を渡す必要があることです。次に例を示します。
// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
ウェブページからメッセージを送信する
拡張機能間のメッセージと同様に、アプリや拡張機能は通常のウェブページから メッセージを受信して応答できます。この機能を使用するには、まず manifest.json で通信するウェブサイトを指定する必要があります。次に例を示します。
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
これにより、指定した URL パターンに一致するすべてのページに Messaging API が公開されます。URL パターンには、少なくともセカンドレベル ドメインを含める必要があります。つまり、「*」、 「*.com」、「*.co.uk」、「*.appspot.com」などのホスト名パターンは禁止されています。ウェブページから、 runtime.sendMessage API または runtime.connect API を使用して、特定のアプリまたは 拡張機能にメッセージを送信します。次に例を示します。
// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
if (!response.success)
handleError(url);
});
アプリまたは拡張機能から、 runtime.onMessageExternal API または runtime.onConnectExternal API を介してウェブページからのメッセージをリッスンできます。これは、拡張機能間の メッセージと同様です。接続を開始できるのはウェブページのみです。次に例を示します。
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url == blocklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
});
ネイティブ メッセージ
拡張機能とアプリは、ネイティブ メッセージ ホストとして登録されているネイティブ アプリケーションとメッセージを交換できます。この機能について詳しくは、ネイティブ メッセージをご覧ください。
セキュリティ上の考慮事項
コンテンツ スクリプトの信頼性は低い
コンテンツ スクリプトは、拡張機能のバックグラウンド ページよりも信頼性が低くなります(たとえば、悪意のあるウェブ ページが、コンテンツ スクリプトが実行されるレンダラ プロセスを侵害する可能性があります)。コンテンツ スクリプトからのメッセージは攻撃者によって作成された可能性があると想定し、すべての入力を検証して サニタイズしてください。コンテンツ スクリプトに送信されたデータがウェブページに漏洩する可能性があると想定してください。 コンテンツ スクリプトから受信したメッセージによってトリガーできる特権アクションの範囲を制限します。
クロスサイト スクリプティング
コンテンツ スクリプトや別の拡張機能からメッセージを受信する場合は、スクリプトがクロスサイト スクリプティングの被害に遭わないように注意する必要があります。このアドバイスは、拡張機能のバックグラウンド ページ内で実行されるスクリプトと、他のウェブオリジン内で実行されるコンテンツ スクリプトの両方に適用されます。 特に、次のような危険な API の使用は避けてください。
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// WARNING! Might be evaluating an evil script!
var resp = eval("(" + response.farewell + ")");
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// WARNING! Might be injecting a malicious script!
document.getElementById("resp").innerHTML = response.farewell;
});
代わりに、スクリプトを実行しない安全な API を使用してください。
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// JSON.parse does not evaluate the attacker's scripts.
var resp = JSON.parse(response.farewell);
});
chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
// innerText does not let the attacker inject HTML elements.
document.getElementById("resp").innerText = response.farewell;
});
例
メッセージを介した通信の簡単な例は、examples/api/messaging ディレクトリにあります。ネイティブ メッセージのサンプルでは、Chrome アプリが ネイティブ アプリと通信する方法を示しています。その他の例とソースコードの表示については、サンプルをご覧ください。