コンテンツ スクリプトは、ウェブページのコンテキストで実行されるファイルです。標準の Document Object Model(DOM)を使用して、ブラウザがアクセスしたウェブページの詳細を読み取り、変更し、親拡張機能に情報を渡すことができます。
コンテンツ スクリプトの機能について
コンテンツ スクリプトは、次の拡張機能 API に直接アクセスできます。
- dom
- i18n
- storage
- runtime.connect()
- runtime.getManifest()
- runtime.getURL()
- runtime.id
- runtime.onConnect
- runtime.onMessage
- runtime.sendMessage()
コンテンツ スクリプトは、他の API に直接アクセスできません。ただし、拡張機能の他の部分とメッセージを交換することで、間接的にアクセスできます。
fetch() などの API を使用して、コンテンツ スクリプトから拡張機能内の他のファイルにアクセスすることもできます。そのためには、それらをウェブアクセス可能なリソースとして宣言する必要があります。これにより、同じサイトで実行されているファーストパーティまたはサードパーティのスクリプトにもリソースが公開されます。
分離された世界で作業する
コンテンツ スクリプトは分離された世界に存在するため、ページや他の拡張機能のコンテンツ スクリプトと競合することなく、JavaScript 環境を変更できます。
拡張機能は、次の例のようなコードを含むウェブページで実行されることがあります。
webPage.html
<html>
  <button id="mybutton">click me</button>
  <script>
    var greeting = "hello, ";
    var button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener(
        "click", () => alert(greeting + button.person_name + "."), false);
  </script>
</html>
この拡張機能は、スクリプトの挿入セクションで説明されているいずれかの手法を使用して、次のコンテンツ スクリプトを挿入できます。
content-script.js
var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
    "click", () => alert(greeting + button.person_name + "."), false);
この変更により、ボタンをクリックすると両方のアラートが順番に表示されます。
スクリプトを挿入する
コンテンツ スクリプトは、静的に宣言することも、動的に宣言することも、プログラムで挿入することもできます。
静的宣言で挿入する
よく知られた一連のページで自動的に実行されるスクリプトには、manifest.json で静的コンテンツ スクリプト宣言を使用します。
静的に宣言されたスクリプトは、マニフェストの "content_scripts" キーの下に登録されます。JavaScript ファイル、CSS ファイル、またはその両方を含めることができます。自動実行されるすべてのコンテンツ スクリプトで、一致パターンを指定する必要があります。
manifest.json
{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["https://*.nytimes.com/*"],
     "css": ["my-styles.css"],
     "js": ["content-script.js"]
   }
 ],
 ...
}
| 名前 | 型 | 説明 | 
|---|---|---|
| matches | array of strings | 必須。このコンテンツ スクリプトが挿入されるページを指定します。これらの文字列の構文の詳細については、一致パターンをご覧ください。URL を除外する方法については、一致パターンとグロブをご覧ください。 | 
| css | array of strings | 省略可。一致するページに挿入する CSS ファイルのリスト。これらは、この配列に表示される順序で、ページの DOM が構築または表示される前に挿入されます。 | 
| js |  | 省略可。一致するページに挿入される JavaScript ファイルのリスト。ファイルは、この配列に表示されている順序で挿入されます。このリスト内の各文字列には、拡張機能のルート ディレクトリ内のリソースへの相対パスが含まれている必要があります。先頭のスラッシュ(`/`)は自動的に削除されます。 | 
| run_at | RunAt | 省略可。スクリプトをページに挿入するタイミングを指定します。デフォルトは document_idleです。 | 
| match_about_blank | ブール値 | 省略可。親フレームまたはオープナー フレームが matchesで宣言されたパターンのいずれかに一致する場合に、スクリプトをabout:blankフレームに挿入するかどうか。デフォルトは false です。 | 
| match_origin_as_fallback | ブール値 | 省略可。スクリプトを、一致するオリジンによって作成されたフレームに挿入するかどうか。ただし、そのフレームの URL またはオリジンがパターンと直接一致しない場合があります。これには、 about:、data:、blob:、filesystem:などのさまざまなスキームのフレームが含まれます。関連フレームへの挿入もご覧ください。 | 
| world | ExecutionWorld | 省略可。スクリプトが実行される JavaScript の世界。デフォルトは ISOLATEDです。分離された世界での作業もご覧ください。 | 
動的宣言で挿入する
動的コンテンツ スクリプトは、コンテンツ スクリプトのマッチパターンが不明な場合や、コンテンツ スクリプトを既知のホストに常に挿入する必要がない場合に便利です。
Chrome 96 で導入された動的宣言は静的宣言に似ていますが、コンテンツ スクリプト オブジェクトは manifest.json ではなく chrome.scripting 名前空間のメソッドを使用して Chrome に登録されます。Scripting API を使用すると、拡張機能のデベロッパーは次のこともできます。
静的宣言と同様に、動的宣言には JavaScript ファイル、CSS ファイル、またはその両方を含めることができます。
service-worker.js
chrome.scripting
  .registerContentScripts([{
    id: "session-script",
    js: ["content.js"],
    persistAcrossSessions: false,
    matches: ["*://example.com/*"],
    runAt: "document_start",
  }])
  .then(() => console.log("registration complete"))
  .catch((err) => console.warn("unexpected error", err))
service-worker.js
chrome.scripting
  .updateContentScripts([{
    id: "session-script",
    excludeMatches: ["*://admin.example.com/*"],
  }])
  .then(() => console.log("registration updated"));
service-worker.js
chrome.scripting
  .getRegisteredContentScripts()
  .then(scripts => console.log("registered content scripts", scripts));
service-worker.js
chrome.scripting
  .unregisterContentScripts({ ids: ["session-script"] })
  .then(() => console.log("un-registration complete"));
プログラムで挿入する
イベントに応答して実行する必要があるコンテンツ スクリプトや、特定の状況で実行する必要があるコンテンツ スクリプトには、プログラムによる挿入を使用します。
コンテンツ スクリプトをプログラムで挿入するには、スクリプトを挿入しようとしているページのホスト権限が必要です。ホスト権限は、拡張機能のマニフェストの一部としてリクエストするか、"activeTab" を一時的に使用することで付与できます。
以下は、activeTab ベースの拡張機能のさまざまなバージョンです。
manifest.json:
{
  "name": "My extension",
  ...
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Action Button"
  }
}
コンテンツ スクリプトはファイルとして挿入できます。
content-script.js
document.body.style.backgroundColor = "orange";
service-worker.js:
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ["content-script.js"]
  });
});
また、関数本体をコンテンツ スクリプトとして挿入して実行することもできます。
service-worker.js:
function injectedFunction() {
  document.body.style.backgroundColor = "orange";
}
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
  });
});
挿入された関数は、元の関数自体ではなく、chrome.scripting.executeScript() 呼び出しで参照される関数のコピーであることに注意してください。そのため、関数の本体は自己完結型である必要があります。関数の外部にある変数への参照があると、コンテンツ スクリプトが ReferenceError をスローします。
関数として挿入する場合は、関数に引数を渡すこともできます。
service-worker.js
function injectedFunction(color) {
  document.body.style.backgroundColor = color;
}
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
    args : [ "orange" ],
  });
});
一致と glob を除外する
指定したページの照合をカスタマイズするには、宣言型登録に次のフィールドを含めます。
| 名前 | 型 | 説明 | 
|---|---|---|
| exclude_matches | array of strings | 省略可。このコンテンツ スクリプトが挿入されるページを除外します。これらの文字列の構文の詳細については、一致パターンをご覧ください。 | 
| include_globs | array of strings | 省略可。 matchesの後に適用され、この glob にも一致する URL のみが含まれます。これは、@includeGreasemonkey キーワードをエミュレートすることを目的としています。 | 
| exclude_globs | 文字列の配列 | 省略可。 matchesの後に適用され、この glob に一致する URL を除外します。@excludeGreasemonkey キーワードをエミュレートすることを目的としています。 | 
次の両方の条件に該当する場合、コンテンツ スクリプトがページに挿入されます。
- URL は matchesパターンとinclude_globsパターンの両方に一致します。
- URL は exclude_matchesパターンまたはexclude_globsパターンにも一致しません。matchesプロパティは必須であるため、exclude_matches、include_globs、exclude_globsは、影響を受けるページを制限するためにのみ使用できます。
次の拡張機能は、コンテンツ スクリプトを https://www.nytimes.com/health に挿入しますが、https://www.nytimes.com/business には挿入しません。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}
service-worker.js
chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  excludeMatches : [ "*://*/*business*" ],
  js : [ "contentScript.js" ],
}]);
glob プロパティは、一致パターンとは異なる、より柔軟な構文に従います。使用できる glob 文字列は、ワイルドカードのアスタリスクと疑問符を含む可能性のある URL です。アスタリスク(*)は、空の文字列を含む任意の長さの任意の文字列に一致し、疑問符(?)は任意の 1 文字に一致します。
たとえば、グロブ https://???.example.com/foo/\* は次のいずれかに一致します。
- https://www.example.com/foo/bar
- https://the.example.com/foo/
ただし、以下には一致しません。
- https://my.example.com/foo/bar
- https://example.com/foo/
- https://www.example.com/foo
この拡張機能は、コンテンツ スクリプトを https://www.nytimes.com/arts/index.html と https://www.nytimes.com/jobs/index.htm* に挿入しますが、https://www.nytimes.com/sports/index.html には挿入しません。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}
この拡張機能は、コンテンツ スクリプトを https://history.nytimes.com と https://.nytimes.com/history に挿入しますが、https://science.nytimes.com や https://www.nytimes.com/science には挿入しません。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}
正しいスコープを実現するために、これらのうちの 1 つ、すべて、または一部を含めることができます。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}
実行日時
run_at フィールドは、JavaScript ファイルがウェブページに挿入されるタイミングを制御します。推奨値とデフォルト値は "document_idle" です。その他の可能な値については、RunAt 型をご覧ください。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "run_at": "document_idle",
      "js": ["contentScript.js"]
    }
  ],
  ...
}
service-worker.js
chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  runAt : "document_idle",
  js : [ "contentScript.js" ],
}]);
| 名前 | 型 | 説明 | 
|---|---|---|
| document_idle | 文字列 | 推奨。可能な限り "document_idle"を使用します。ブラウザは、 "document_end"とwindow.onloadイベントの直後の間でスクリプトを挿入するタイミングを選択します。挿入の正確なタイミングは、ドキュメントの複雑さと読み込みにかかる時間によって異なり、ページの読み込み速度に合わせて最適化されます。"document_idle"で実行されるコンテンツ スクリプトは、window.onloadイベントをリッスンする必要はありません。DOM の完了後に実行されることが保証されています。スクリプトがwindow.onloadの後に実行される必要がある場合、拡張機能はdocument.readyStateプロパティを使用して、onloadがすでに呼び出されているかどうかを確認できます。 | 
| document_start | 文字列 | スクリプトは、 cssのファイルの後、他の DOM が構築される前、または他のスクリプトが実行される前に挿入されます。 | 
| document_end | 文字列 | スクリプトは、DOM が完了した直後、画像やフレームなどのサブリソースが読み込まれる前に挿入されます。 | 
フレームを指定する
マニフェストで指定された宣言型コンテンツ スクリプトの場合、"all_frames" フィールドを使用すると、指定された URL 要件に一致するすべてのフレームに JavaScript ファイルと CSS ファイルを挿入するか、タブの最上位フレームのみに挿入するかを指定できます。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "all_frames": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}
chrome.scripting.registerContentScripts(...) を使用してコンテンツ スクリプトをプログラムで登録する場合、allFrames パラメータを使用して、指定された URL 要件に一致するすべてのフレームにコンテンツ スクリプトを挿入するか、タブの最上位フレームのみに挿入するかを指定できます。これは tabId でのみ使用でき、frameIds または documentIds が指定されている場合は使用できません。
service-worker.js
chrome.scripting.registerContentScripts([{
  id: "test",
  matches : [ "https://*.nytimes.com/*" ],
  allFrames : true,
  js : [ "contentScript.js" ],
}]);
関連フレームに挿入
拡張機能では、一致するフレームに関連付けられているが、それ自体は一致しないフレームでスクリプトを実行したい場合があります。この状況が発生する一般的なシナリオとしては、一致するフレームによって作成された URL を持つフレームがあり、その URL 自体はスクリプトで指定されたパターンと一致しない場合などがあります。
これは、拡張機能が about:、data:、blob:、filesystem: スキームの URL を持つフレームに挿入する場合です。このような場合、URL はコンテンツ スクリプトのパターンと一致しません(また、about: と data: の場合、about:blank や data:text/html,<html>Hello, World!</html> のように、URL に親 URL やオリジンがまったく含まれていません)。ただし、これらのフレームは作成フレームに関連付けることができます。
これらのフレームに挿入するには、拡張機能でマニフェストのコンテンツ スクリプト仕様に "match_origin_as_fallback" プロパティを指定できます。
manifest.json
{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "match_origin_as_fallback": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}
指定されていて true に設定されている場合、Chrome はフレームの URL ではなく、フレームのイニシエータのオリジンを見て、フレームが一致するかどうかを判断します。これは、ターゲット フレームのオリジン(data: URL のオリジンは null です)。
フレームのイニシエータは、ターゲット フレームを作成またはナビゲートしたフレームです。通常は直接の親またはオープナーですが、そうでない場合もあります(iframe 内の iframe をナビゲートするフレームの場合など)。
これはイニシエータ フレームのオリジンを比較するため、イニシエータ フレームはそのオリジンからの任意のパスに存在できます。この意味を明確にするため、Chrome では、"match_origin_as_fallback" が true に設定されているコンテンツ スクリプトは、* のパスも指定する必要があります。
"match_origin_as_fallback" と "match_about_blank" の両方が指定されている場合、"match_origin_as_fallback" が優先されます。
埋め込みページとの通信
コンテンツ スクリプトの実行環境と、それをホストするページは互いに分離されていますが、ページの DOM へのアクセスは共有しています。ページがコンテンツ スクリプトと通信する場合、またはコンテンツ スクリプトを介して拡張機能と通信する場合は、共有 DOM を介して行う必要があります。
window.postMessage() を使用した例を次に示します。
content-script.js
var port = chrome.runtime.connect();
window.addEventListener("message", (event) => {
  // We only accept messages from ourselves
  if (event.source !== window) {
    return;
  }
  if (event.data.type && (event.data.type === "FROM_PAGE")) {
    console.log("Content script received: " + event.data.text);
    port.postMessage(event.data.text);
  }
}, false);
example.js
document.getElementById("theButton").addEventListener("click", () => {
  window.postMessage(
      {type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);
拡張機能以外のページ(example.html)は、自身にメッセージを投稿します。このメッセージはコンテンツ スクリプトによってインターセプトされて検査され、拡張機能プロセスに投稿されます。このようにして、ページは拡張機能プロセスへの通信ラインを確立します。同様の方法で逆の操作も可能です。
拡張機能ファイルにアクセスする
コンテンツ スクリプトから拡張機能ファイルにアクセスするには、次の例(content.js)に示すように、chrome.runtime.getURL() を呼び出して拡張機能アセットの絶対 URL を取得します。
content-script.js
let image = chrome.runtime.getURL("images/my_image.png")
CSS ファイルでフォントや画像を使用するには、次の例(content.css)に示すように、@@extension_id を使用して URL を構築します。
content.css
body {
 background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}
@font-face {
 font-family: 'Stint Ultra Expanded';
 font-style: normal;
 font-weight: 400;
 src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}
すべてのアセットは、manifest.json ファイルでウェブ アクセス可能なリソースとして宣言する必要があります。
manifest.json
{
 ...
 "web_accessible_resources": [
   {
     "resources": [ "images/*.png" ],
     "matches": [ "https://example.com/*" ]
   },
   {
     "resources": [ "fonts/*.woff" ],
     "matches": [ "https://example.com/*" ]
   }
 ],
 ...
}
コンテンツ セキュリティ ポリシー
分離されたワールドで実行されるコンテンツ スクリプトには、次のコンテンツ セキュリティ ポリシー(CSP)があります。
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
他の拡張機能コンテキストに適用される制限と同様に、これにより eval() の使用と外部スクリプトの読み込みが禁止されます。
解凍された拡張機能の場合、CSP に localhost も含まれます。
script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* chrome-extension://abcdefghijklmopqrstuvwxyz/; object-src 'self';
コンテンツ スクリプトがメインワールドに挿入されると、ページの CSP が適用されます。
安全を確保する
分離されたワールドは保護レイヤを提供しますが、コンテンツ スクリプトを使用すると、拡張機能とウェブページに脆弱性が生じる可能性があります。コンテンツ スクリプトが fetch() の呼び出しなどによって別のウェブサイトからコンテンツを受け取る場合は、コンテンツを挿入する前に、クロスサイト スクリプティング攻撃に対するフィルタリングを必ず行ってください。"man-in-the-middle"攻撃を回避するため、HTTPS 経由でのみ通信します。
必ず悪意のあるウェブページでフィルタしてください。たとえば、次のパターンは危険であり、マニフェスト V3 では許可されていません。
content-script.js
const data = document.getElementById("json-data"); // WARNING! Might be evaluating an evil script! const parsed = eval("(" + data + ")");
content-script.js
const elmt_id = ... // WARNING! elmt_id might be '); ... evil script ... //'! window.setTimeout("animate(" + elmt_id + ")", 200);
代わりに、スクリプトを実行しない安全な API を使用してください。
content-script.js
const data = document.getElementById("json-data") // JSON.parse does not evaluate the attacker's scripts. const parsed = JSON.parse(data);
content-script.js
const elmt_id = ... // The closure form of setTimeout does not evaluate scripts. window.setTimeout(() => animate(elmt_id), 200);