メッセージ受け渡し

コンテンツ スクリプトは、実行する拡張機能ではなくウェブページのコンテキストで実行されるため、多くの場合、拡張機能の他の部分と通信する方法が必要になります。たとえば、RSS リーダー拡張機能は、コンテンツ スクリプトを使用してページ上の RSS フィードの存在を検出し、そのページのアクション アイコンを表示するようにサービス ワーカーに通知します。

この通信ではメッセージ パスが使用されます。これにより、拡張機能とコンテンツ スクリプトの両方が互いのメッセージをリッスンし、同じチャネルで応答できます。メッセージには、有効な JSON オブジェクト(null、ブール値、数値、文字列、配列、オブジェクト)を含めることができます。メッセージ パススルー API は 2 つあります。1 つは1 回限りのリクエスト用で、もう 1 つは複数のメッセージを送信できる長時間接続用です。拡張機能間でのメッセージの送信については、拡張機能間のメッセージのセクションをご覧ください。

1 回限りのリクエスト

拡張機能の別の部分に 1 つのメッセージを送信し、必要に応じてレスポンスを取得するには、runtime.sendMessage() または tabs.sendMessage() を呼び出します。これらのメソッドを使用すると、コンテンツ スクリプトから拡張機能、または拡張機能からコンテンツ スクリプトに対して、JSON シリアル化可能な 1 回限りのメッセージを送信できます。レスポンスを処理するには、返された Promise を使用します。古い拡張機能との下位互換性を維持するには、代わりに最後の引数としてコールバックを渡します。同じ呼び出しで Promise とコールバックを使用することはできません。

メッセージを送信すると、メッセージを処理するイベント リスナーにオプションの 3 番目の引数 sendResponse が渡されます。これは、メッセージを送信した関数の戻り値として使用される JSON シリアル化可能なオブジェクトを受け取る関数です。デフォルトでは、sendResponse コールバックは同期的に呼び出す必要があります。非同期処理を実行して sendResponse に渡される値を取得する場合は、イベント リスナーからリテラル true(真偽値だけでなく)を返す必要があります。これにより、sendResponse が呼び出されるまで、メッセージ チャネルがもう一方の端と開いたままになります。

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must send an explicit `true`
  return true;
}

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

コールバックを Promise に変換して拡張機能で使用する方法については、Manifest V3 移行ガイドをご覧ください。

コンテンツ スクリプトからリクエストを送信するコードは次のようになります。

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

メッセージに同期的に返信する場合は、返信を受け取ったら sendResponse を呼び出し、完了したことを示す false を返します。非同期で応答するには、true を返して、使用できる状態になるまで sendResponse コールバックをアクティブにします。非同期関数は、サポートされていない Promise を返すため、サポートされていません。

コンテンツ スクリプトにリクエストを送信するには、次の例に示すように、リクエストが適用されるタブを指定します。この例は、Service Worker、ポップアップ、タブとして開いた chrome-extension:// ページで機能します。

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

メッセージを受信するには、runtime.onMessage イベント リスナーを設定します。拡張機能とコンテンツ スクリプトの両方で同じコードを使用します。

content-script.js または service-worker.js:

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; を追加します。

複数のページが onMessage イベントをリッスンしている場合、特定のイベントに対して最初に sendResponse() を呼び出したページのみがレスポンスを送信できます。そのイベントに対する他のすべてのレスポンスは無視されます。

長時間継続する接続

再利用可能な長時間のメッセージ パススルー チャネルを作成するには、runtime.connect() を呼び出してコンテンツ スクリプトから拡張機能ページにメッセージを渡すか、tabs.connect() を呼び出して拡張機能ページからコンテンツ スクリプトにメッセージを渡します。チャンネルに名前を付けて、さまざまなタイプの接続を区別できます。

長時間の接続のユースケースとして、フォーム自動入力拡張機能があります。コンテンツ スクリプトは、特定のログイン用の拡張機能ページへのチャネルを開き、ページ上の入力要素ごとに拡張機能にメッセージを送信して、入力するフォームデータをリクエストします。共有接続により、拡張機能は拡張機能コンポーネント間で状態を共有できます。

接続を確立すると、各エンドに runtime.Port オブジェクトが割り当てられ、その接続を介してメッセージの送受信が行われます。

次のコードを使用して、コンテンツ スクリプトからチャネルを開き、メッセージを送受信します。

content-script.js:

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"});
});

拡張機能からコンテンツ スクリプトにリクエストを送信するには、前の例の runtime.connect() の呼び出しを tabs.connect() に置き換えます。

コンテンツ スクリプトまたは拡張機能ページの受信接続を処理するには、runtime.onConnect イベント リスナーを設定します。拡張機能の別の部分で connect() が呼び出されると、このイベントと runtime.Port オブジェクトがアクティブになります。受信接続に応答するコードは次のようになります。

service-worker.js:

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() を呼び出すと、タブ内のフレームごとに 1 回ずつ runtime.onConnect イベントが呼び出されます。同様に、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 メソッドを使用します。それぞれの例を次に示します。

service-worker.js

// For a single request:
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 を次のように渡します。

service-worker.js

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

ウェブページからメッセージを送信する

拡張機能は、他のウェブページからのメッセージを受信して返信することもできますが、ウェブページにメッセージを送信することはできません。ウェブページから拡張機能にメッセージを送信するには、"externally_connectable" マニフェスト キーを使用して、通信するウェブサイトを manifest.json で指定します。次に例を示します。

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

これにより、指定した URL パターンに一致するすべてのページに Messaging API が公開されます。URL パターンには、少なくともセカンダリ ドメインを含める必要があります。つまり、「*」、「*.com」、「*.co.uk」、「*.appspot.com」などのホスト名パターンはサポートされていません。Chrome 107 以降では、<all_urls> を使用してすべてのドメインにアクセスできます。これはすべてのホストに影響するため、これを使用している拡張機能の Chrome ウェブストア審査は時間がかかる可能性があります

runtime.sendMessage() または runtime.connect() API を使用して、特定のアプリまたは拡張機能にメッセージを送信します。次に例を示します。

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (response) => {
      if (!response.success) handleError(url);
    }
  );
}

拡張機能間のメッセージ送信の場合と同様に、拡張機能から runtime.onMessageExternal または runtime.onConnectExternal API を使用してウェブページからのメッセージをリッスンします。次の例をご覧ください。

service-worker.js

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 など、信頼できないソースからデータを受け取る場合は、そのデータを HTML として解釈したり、予期しないコードの実行を許可する方法で使用したりしないように注意してください。

より安全な方法

可能な限り、スクリプトを実行しない API を使用してください。

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
安全でないメソッド

拡張機能の脆弱性を高める次の方法は使用しないでください。

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});