コンテンツ スクリプト

コンテンツ スクリプトは、ウェブページのコンテキストで実行されるファイルです。標準のドキュメント オブジェクト モデル(DOM)を使用して、ブラウザがアクセスしたウェブページの詳細を読み取り、変更を加え、親拡張機能に情報を渡すことができます。

コンテンツ スクリプトの機能について

コンテンツ スクリプトは、次の拡張機能 API に直接アクセスできます。

コンテンツ スクリプトは他の 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" ],
  });
});

一致とグロブを除外する

指定したページ マッチングをカスタマイズするには、宣言型の登録に次のフィールドを含めます。

名前 説明
exclude_matches array of strings 省略可。このコンテンツ スクリプトが挿入されるページを除外します。これらの文字列の構文の詳細については、一致パターンをご覧ください。
include_globs array of strings 省略可。matches の後に適用され、このグロブにも一致する URL のみが含まれます。これは、Greasemonkey キーワード @include をエミュレートすることを目的としています。
exclude_globs 文字列の配列 省略可。matches の後に適用され、このグロブに一致する URL を除外します。@exclude Greasemonkey キーワードをエミュレートすることを目的としています。

コンテンツ スクリプトは、次の両方の条件に該当する場合にページに挿入されます。

  • URL は、任意の matches パターンと任意の include_globs パターンに一致します。
  • URL は exclude_matches または exclude_globs パターンとも一致しません。matches プロパティは必須であるため、exclude_matchesinclude_globsexclude_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 プロパティの構文は、一致パターンとは異なり、より柔軟です。使用できるグロブ文字列は、「ワイルドカード」のアスタリスクと疑問符を含む 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.htmlhttps://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.comhttps://.nytimes.com/history に挿入しますが、https://science.nytimes.comhttps://www.nytimes.com/science には挿入しません。

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

これらのいずれか、またはすべてを含めて、適切なスコープを実現できます。

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" フィールドを使用して、JavaScript ファイルと CSS ファイルを、指定された URL 要件に一致するすべてのフレームに挿入するか、タブの最上位のフレームにのみ挿入するかを拡張機能で指定できます。

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:blankdata: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 を移動するフレームの場合など)。

これはイニシエータ フレームの起点を比較するため、イニシエータ フレームは起点からの任意のパスにある可能性があります。この意味を明確にするために、"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/*" ]
   }
 ],
 ...
}

安全の確保

分離されたワールドは保護レイヤを提供しますが、コンテンツ スクリプトを使用すると、拡張機能とウェブページに脆弱性が生じる可能性があります。コンテンツ スクリプトが fetch() を呼び出すなどして別のウェブサイトからコンテンツを受信する場合は、コンテンツを挿入する前に、クロスサイト スクリプティング攻撃に対してコンテンツをフィルタするようにしてください。"man-in-the-middle"攻撃を回避するため、HTTPS 経由でのみ通信します。

悪意のあるウェブページをフィルタするようにしてください。たとえば、次のパターンは危険であり、Manifest 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);