このページはコミュニティーの尽力で英語から翻訳されました。MDN Web Docs コミュニティーについてもっと知り、仲間になるにはこちらから。

View in English Always switch to English

コンテンツスクリプト

コンテンツスクリプトは、ウェブページのコンテキストで実行される拡張機能の一部です。標準の Web API を使用して、ページの内容を読み込んだり変更したりすることができます。コンテンツスクリプトの動作は、<script> 要素などを使用して読み込まれた、ウェブサイトの一部を構成するスクリプトと同様です。ただし、コンテンツスクリプトがページコンテンツにアクセスできるのは、ウェブページのオリジンに対するホスト権限が付与されている場合に限られます。

コンテンツスクリプトは、WebExtension API のごく一部 にアクセスできますが、メッセージングシステムを使用してバックグラウンドスクリプトと通信することで、間接的に WebExtension API にアクセスすることができます。バックグラウンドスクリプトは、すべての WebExtension JavaScript API にアクセス可能ですが、ウェブページのコンテンツには直接アクセスできません。

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

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

インストール時に、URL パターンに一致するページ内へ。

manifest.jsoncontent_scripts キーを使用して、URL が指定されたパターンに一致するページをロードするたびにコンテンツスクリプトを読み込むようブラウザーに依頼できます。

実行時に、URL パターンに一致するページ内へ。

scripting.registerContentScripts() または(Firefox であればマニフェスト V2 の) contentScripts を使って、URL が指定されたパターンに一致するページを読み込むたびにコンテンツスクリプトを読み込むようブラウザーに依頼できます。これは方法 1 と似ていますが、実行時にコンテンツスクリプトを追加/削除できる点が異なります。)

実行時に、特定のタブへ。

scripting.executeScript() または(マニフェスト V2 のみ) tabs.executeScript() を使用すると、必要なときにコンテンツスクリプトを特定のタブに読み込むことができます。(ユーザーがブラウザーアクションをクリックした場合など。)

グローバルスコープはフレームごと、拡張機能ごとに 1 つしか存在しません。つまり、コンテンツスクリプトの変数は、そのコンテンツスクリプトがどのように読み込まれたかに関係なく、他のどのコンテンツスクリプトからもアクセス可能です。

メモ: ダイナミック JS モジュールインポートがコンテンツスクリプトで動作するようになりました。詳しくはFirefox バグ 1536094を参照してください。 moz-extension スキームを持つ URL のみが許可され、data URL は除外されます (Firefox バグ 1587336)。

永続化

scripting.executeScript() または(マニフェスト V2 のみ)tabs.executeScript() で読み込まれたコンテンツスクリプトは、リクエストに応じて動作し、永続化されません。

マニフェストファイルの content_scripts キーで定義されたコンテンツスクリプト、または scripting.registerContentScripts() もしくは (Firefox のマニフェスト V2 でのみ) contentScripts を使用して定義されたコンテンツスクリプト API によって定義されたコンテンツスクリプトは、デフォルトで永続化されます。これらは、ブラウザーの再起動や更新、そして拡張機能の再起動後も登録されたままとなります。

ただし、scripting.registerContentScripts() API はスクリプトを永続的でないものとして定義する機能を提供しています。これは、例えば、拡張機能が(ユーザーに代わって)現在のブラウザーセッション中のみコンテンツスクリプトを有効にしたい場合などに有益です。

権限、制約、制限

権限

登録されたコンテンツスクリプトは、その拡張機能にドメインのホスト権限が付与されている場合にのみ実行されます。

プログラムでスクリプトを挿入するには、拡張機能には activeTab 権限またはホスト権限のどちらかが必要です。 scripting 権限は scripting API のメソッドを使用するために必要となります。

インストール時、拡張機能は、content_scripts マニフェストキーの matches リストに含まれるホストに対して、ホスト権限をリクエストすることができます。ユーザーは、拡張機能のインストール後、ホスト権限の許可または拒否を選択できます。

ドメインの制限

ホスト権限activeTab 権限はどちらも、いくつかのドメインが例外になっています。例えば、拡張機能が特別なページを通して権限をエスカレートさせることからユーザーを保護するために、コンテンツスクリプトはこの例ではドメイン上での実行がブロックされます。

Firefoxでは、これには以下のドメインが含まれます。

  • accounts-static.cdn.mozilla.net
  • accounts.firefox.com
  • addons.cdn.mozilla.net
  • addons.mozilla.org
  • api.accounts.firefox.com
  • content.cdn.mozilla.net
  • discovery.addons.mozilla.org
  • install.mozilla.org
  • oauth.accounts.firefox.com
  • profile.accounts.firefox.com
  • support.mozilla.org
  • sync.services.mozilla.com

他のブラウザーでも、拡張機能をインストールできるウェブサイトには同様の制限があります。例えば、 Chrome では chrome.google.com へのアクセスが制限されています。

メモ: これらの制限は addons.mozilla.org を含んでいるので、ユーザーはインストール直後に拡張機能を使用しようとし、それが動作しないことに気付くかもしれません。 適切な警告を追加したり、オンボーディングページを追加して、ユーザーを addons.mozilla.org から遠ざけたりするとよいでしょう。

一連のドメインは、エンタープライズポリシーでさらに制限することができます。 Firefox は ExtensionSettings in mozilla/policy-templates で文書化されている restricted_domains ポリシーを認識します。Chrome の runtime_blocked_hosts ポリシーは Configure ExtensionSettings policy で文書化されています。

制限

デフォルトで、コンテンツスクリプトは about:blankabout:srcdocdata:blob: のページでは実行されません。これらのスクリプトの実行を有効にするには、content_scripts マニフェストキー内の match_origin_as_fallback オプション、または scripting API の matchOriginAsFallback オプションを使用してください。

拡張機能は、特権のあるブラウザー UI ページ(about:debuggingabout:addons、リーダービュー、ソース表示、PDF ビューアなど)や拡張機能ページにはコンテンツスクリプトを挿入できません。

拡張機能が拡張機能ページ内で動的にコードを実行したい場合は、そのページにスクリプトを含めることができます。このスクリプトには実行するコードが含まれており、コードを実行する方法を実装した runtime.onMessage リスナーを登録します。その後、拡張機能はリスナーにメッセージを送信して、コードの実行を開始することができます。

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

DOM アクセス

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

しかし、コンテンツスクリプトは DOM の「きれいな」見た目を取得します。すなわち、

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

Chrome の非互換性における「コンテンツスクリプト環境」で触れている通り、動作はブラウザー間で異なります。

  • Firefox では、この挙動は Xray vision と呼ばれます。 コンテンツスクリプトでは、グローバルスコープの JavaScript オブジェクトや、ウェブページから Xray でラップされたバージョンのオブジェクトに遭遇することがあります。通常のウェブページでは、globalThiswindow と同一ですが、Firefox のコンテンツスクリプトでは、globalThiswindow を継承する独立したオブジェクトとなります。この違いは、グローバル API の利用可能性に関して、多くの場合、実際にはほとんど影響を与えません。例外となるのは、グローバルスコープに、window 内の定義をシャドウする標準 API の定義が含まれている場合です。例としては、コンテンツスクリプトにおける structuredClone が挙げられます。

  • Chromeでは、この動作は隔離された世界 (isolated world) によって強制され、根本的に異なる手法を使用しています。

次のようなウェブページを考えてみてください。

html
<!doctype html>
<html lang="ja">
  <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 スクリプトは次を実行します。

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 = () => {
  alert("The page script has also redefined 'confirm'");
};

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

js
// content-script.js

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

// can't see properties added by page-script.js
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 ライブラリーを使用する必要がある場合、そのライブラリー自体は、それを使用したいコンテンツスクリプトと 並べて 挿入すべきです。

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

メモ: Firefox では cloneInto() および exportFunction() により、コンテンツスクリプトがページスクリプトによって作成された JavaScript オブジェクトにアクセスし、その JavaScript オブジェクトをページスクリプトに公開することができるようにしたりしています。

詳しくはページスクリプトとオブジェクトを共有するのページを見てください。

WebExtension API

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

extension から:

runtime から:

i18n から:

menus から:

すべてから:

XHR と Fetch

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

メモ: Firefox では、コンテンツスクリプトの(例えば、fetch() を使った)リクエストは、拡張機能のコンテキストで起こるので、ページコンテンツを参照する URL を絶対 URL で提供せねばなりません。

Chrome では、リクエストはページのコンテキストで起こるので、相対 URL で行われます。例えば、/apihttps://[現在のページの URL]/api に送られます。

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

メモ: Manifest V3 を使用する場合、出力先サーバーが CORS を使用してオプトインするとき、コンテンツスクリプトはオリジン間リクエストを実行できます。ただし、コンテンツスクリプトではホスト権限は動作しませんが、通常の拡張ページではまだ動作しています。

これは、コンテンツスクリプトでより特権的な XHR とフェッチインスタンスを公開することによって達成されます。これは、ページ自身からのリクエストのように、Origin および Referer ヘッダーを設定しない副作用があります。これは、クロスオリジンの性質を明らかにしないリクエストを行うにはよく望ましいとされることです。

メモ: マニフェスト V2 の Firefox では、コンテンツ自身によって送信されたかのように振る舞うリクエストを実行する必要がある拡張機能は、代わりに content.XMLHttpRequestcontent.fetch() を使用することができます。

クロスブラウザー拡張機能にとってこれらの存在は機能検出となります。

Manifest V3 では content.XMLHttpRequestcontent.fetch() が利用できないため、このようなことは起こりえません。

メモ: Chrome ではバージョン 73 から、Firefox ではバージョン 101 からマニフェスト V3 を使用する場合、コンテンツスクリプトは、その中で実行されるページと同じ CORS ポリシーが適用されるようになりました。バックエンドスクリプトのみ、昇格したクロスドメイン特権があります。Chrome Extension コンテンツスクリプトにおける Cross-Origin Requests の変更点を参照してください。

保護されたコンテキスト

HTTPS または localhost などの信頼できるソースから読み込まれたページは、保護されたコンテキストを提供します。crypto.subtlenavigator.geolocation などの一部の Web API は、保護されたコンテキストでのみ利用可能です。これらの制限付き API では、攻撃者によって改ざんされる可能性のあるウェブページ上で情報や機能を公開します。

コンテンツスクリプトは、挿入されたページのコンテキスト内で実行されます。したがって、これらの API に対する制限はコンテンツスクリプトにも適用されます。つまり、セキュリティ保護されていないコンテキストで実行されているコンテンツスクリプトは、拡張機能の他の部分がその API にアクセスできる場合でも、保護されたコンテキストを要求する Web API を使用することはできません。

メモ: Firefox では、保護されたコンテキスト限定の API PointerEvent.getCoalescedEvents() が、非保護されたコンテキストのコンテンツスクリプトから呼び出されることが可能です。

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

コンテンツスクリプトは WebExtension の API のほとんどを直接使用することはできませんが、メッセージング API を使用して拡張機能のバックグラウンドスクリプトと通信できるため、バックグラウンドスクリプトが使用できるのと同じ API にすべて間接的にアクセスすることができます。

バックグラウンドスクリプトとコンテンツスクリプトの間の通信には、基本的な 2 つのパターンがあります。

  • 単発のメッセージ(オプションのレスポンス付き)を送信することができます。
  • 両者の間に長寿命のコネクションを設定し、そのコネクションを使用してメッセージを交換することができます。

単発のメッセージ

レスポンスが必須でない単発のを送るには、次の API を使います。

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

例えば、ウェブページでのクリックイベントを待ち受けするコンテンツスクリプトがここにあります。

クリックがリンク上である場合、ターゲット URL をバックグラウンドページにメッセージします。

js
// 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 を使って通知を表示します。

js
// background-script.js

browser.runtime.onMessage.addListener(notify);

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

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

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

バックグラウンドスクリプトとコンテンツスクリプトの間で多くのメッセージを交換する場合、単発のメッセージの送信は面倒になることがあります。そこで、 2 つのコンテキスト間でより詳しい接続を確立し、この接続を使用してメッセージを交換するという方法があります。

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

コネクションを作成するには次のようにします。

  • 片方で runtime.onConnect にてコネクションを待ち受けする。
  • もう片方で次を呼び出す。

これは runtime.Port オブジェクトを返します。

それぞれがポートを持ったら、両方が、

  • runtime.Port.postMessage() でメッセージを送って
  • runtime.Port.onMessage でメッセージを受信できるようになる。

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

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

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

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

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

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

  • コンテンツスクリプトからの通信試行を待ち受けする

  • 通信試行を受け取ったとき、

    • portFromCS という名前の変数にポートを格納する
    • そのポートを使ってコンテンツスクリプトにメッセージを送る
    • ポートに届いたメッセージを待ち受けしてログに出す
  • ユーザーが拡張機能のブラウザーアクションをクリックしたとき、portFromCS を使ってコンテンツスクリプトにメッセージを送る

js
// background-script.js

let portFromCS;

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

browser.runtime.onConnect.addListener(connected);

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

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

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

js
// background-script.js

let ports = [];

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

browser.runtime.onConnect.addListener(connected);

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

単発メッセージとコネクションベースのメッセージとの選択

単発とコネクションベースのメッセージの選択は、拡張機能がどうメッセージを利用すると期待されるかに依存します。

推奨されるベストプラクティスは、次の通りです。

  • 単発メッセージを使用する場合
    • メッセージに 1 つだけの応答がある場合
    • メッセージの受信を少しのスクリプトが待ち受けする場合(runtime.onMessage 呼び出し)
  • コネクションベースのメッセージを使用する場合
    • スクリプトが、複数のメッセージを交換するセッションに関わる場合
    • 拡張機能がタスクの進捗や、タスクが中断されたのを知る必要がある場合、または初期化されたタスクをメッセージング経由で中断したい場合

ウェブページとの通信

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

例えば:

js
// page-script.js

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

messenger.addEventListener("click", messageContentScript);

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

window.addEventListener("message", (event) => {
  if (
    event.source === window &&
    event?.data?.direction === "from-page-script"
  ) {
    alert(`Content script received message: "${event.data.message}"`);
  }
});

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

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

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

js
// content-script.js

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

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

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

メモ: eval() はマニフェスト V3 では利用できません。

Chrome では

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

Firefox では

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

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

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

js
// 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() を用いて作成し、値をログに出し、ページにメッセージします。

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

js
window.addEventListener("message", (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() にも言えます。

警告: ページのコンテキストでコードを実行するときは特に注意してください!

ページの環境が悪意をはらんだウェブページにコントロールされ、期待しない方法であなたが操作するオブジェクトを再定義するかもしれません。

js
// page.js redefines console.log

let original = console.log;

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

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