風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

Chrome拡張機能Manifest V3対応の勘どころ(?)

Chrome ウェブストアに登録している4つの拙作Chrome拡張機能Manifest V3への対応が先日ようやく完了しました

Manifest V3対応のために当方が実際に行った作業の概要やハマった点などを備忘として残しておきます。
詳細でなかったり整理できていない点に関してはご容赦ください



何はともあれ、公式ページを見ておくべし

developer.chrome.com
手探りでいろいろと検索して調べたりしましたが、結局は公式ページに書いてあることでした……というパターンも多々あったため、ひととおり目を通しておくことをおすすめします。
といいながら、英語が苦手な自分は見てない所も多いんですけどね……(汗)

Firefoxとコードを共通化したいんだけど?

現在(2022/10/1)のところ、Firefox(Web Extensions)ではManifest V3には(一般向けとしては)未対応です(2022年末に向けてサポート予定)。

でも製作者としては、manifest.jsonは切り替えるにしても、JavaScriptのコードはなるべく共通化したいという場合もあるかと思います。
その場合、

const MANIFEST_VERSION = chrome.runtime.getManifest().manifest_version;

のようにすれば、manifest.jsonで定義した"manifest_version"の値を取得できるので、コード内であらかじめManifestのバージョンを取得しておき、随時場合分けすることで対応することができるかと思います。

backgroundはService Workerに変更

Manifest V3では、backgroundページはService Workerに置き換わっています
V2ではmanifest.jsonに

    "background" : {
        "scripts" : [ "js/background.js" ],
        "persistent": true
    }

みたいに記述していたのが、V3では

    "background" : {
        "service_worker" : "js/background.js"
    }

のようになります。
注意点として、V2では配列で複数のスクリプトを指定できていたのが、V3では指定可能なスクリプトは1つだけです。
なのでV3で複数のスクリプトを指定したい場合には、メインとして指定したスクリプトからimportScripts()("background"で"type": "module"オプション(ES Module)を設定した場合にはimport)を使ってその他のスクリプト(ライブラリ)を読み込む必要があります。
"persistent"オプションは無くなったようです(常に"persistent": falseになるイメージか?)。
また、"type": "module"とすると、ES Moduleとして動作するらしいです(試していないです)。

Manifest V3では削除されてしまったAPIに注意

もともとV2時代から非推奨(deprecated)となっていたAPIのいくつかは、V3になると削除されて使えなくなっているので要注意です
古くからある拡張機能だと、chrome.extension.xx といったAPIが残っていたりすることもあると思われます。
自分の場合、chrome.extension.getURL()等が残っていてはまりました

manifest.json上"permissions"の指定方法変更(ホスト指定の分離)

Manifest V2では

"permissions": [
    "storage",
    "webRequest", 
    "webRequestBlocking", 
    "*://*.twitter.com/*", 
    "*://pbs.twimg.com/*",
    "*://video.twimg.com/*",
    "*://*.cdn.vine.co/*"
]

のように、機能の使用権限とリソース(URL)へのアクセス許可とが混在していましたが、V3では

"permissions" : [
        "storage",
        "declarativeNetRequest",
        "declarativeNetRequestFeedback"
],
"host_permissions" : [
    "*://*.twitter.com/*", 
    "*://pbs.twimg.com/*",
    "*://video.twimg.com/*",
    "*://*.cdn.vine.co/*"
]

のように明確に分離されています(permissionsはhost_permissionsに、optional_permissionsはoptional_host_permissionsに、それぞれ対応しているようです)。
なお、declarativeNetRequestのルールが適用されるホストについては、ほとんどの場合はhost_permissionsによる許可の方は不要ですが、一部必要となる場合もあります(リダイレクト時・ヘッダ変更時など)

Action APIについての変更(browser_actionとpage_actionの統合)

Manifest V2にあった"browser_action"と"page_action"はV3では"action"に統一されているようです
manifest.json上では単に"browser_action"/"page_action"→"action"に変更・統合すればよいのですが、JavaScriptのソースコード中でchrome.browserAction/chrome.pageActionとなっている箇所を探して書き換えないといけないので、

chrome.browserAction.setIcon( { path : icon_path } );

( chrome.action || chrome.browserAction ).setIcon( { path : icon_path } );

若干手間ですね。

content_scripts等から拡張機能内のファイルにアクセスしたい場合は?(Web-accessible resourcesの指定方法変更)

Web-accessible resourcesの扱いも(manifest.jsonでの定義方法)も、Manifest V2とV3では異なっています
例えばV2だと

"web_accessible_resources" : [
        "scripts/*.js"
]

のように単にアクセス許可を出したいリソースのパスだけを指定すればよかったものが、V3だと少なくとも

"web_accessible_resources" : [{
        "resources" : [ "scripts/*.js" ],
        "matches" : [ "*://*.twitter.com/*" ]
}]

のように、どのサイトからアクセスが可能かを明示する必要があります。
他にもリソースにアクセスできる拡張機能をIDで指定できるなどの設定が追加されています(詳細は公式ページを参照)。

background内でwindowにアクセスできなくなった!

Manifest V3だと、background(Service Worker)ではDOMやwindowオブジェクトは存在しません(undefined)。

自分の場合、V2ではwindowはグローバルなオブジェクトとしての利用で、オプション画面(options_ui)に提供したい関数を生やす(options_ui側でchrome.extension.getBackgroundPage()によりbackgroundのwindowを取得)という使い方でした。
V3ではそもそもこういう使い方はできないのですが、ソースコードを共通化するための便宜上、V3でのグローバルなオブジェクトであるselfを暫定的に割り当てています。

((window) => {
// V2/V3共通のbackgroundの処理
})(
    (typeof window !== 'undefined' ? window : self)
);

当然ながらこの方法だとたとえばwindowサイズを知りたい等の用途では使えませんので、必要に応じてその都度別のやり方を検討する必要があります。

background内でlocalStorageが使えなくなった!

Manifest V2では、background内でlocalStorageが使えたので、オプション設定の記憶用等に使っていることもあるかと思いますが、これはV3では使えなくなってしまいます。
代わりにchrome.storage APIが使えますが、

  • localStorageは同期的に使えましたが、chrome.storage.localは非同期です
    これが面倒でいつまでも置き換えなかったツケが今回回ってきました…
  • 当然localStorageとの互換性はないので、既存のバージョンで保持していたオプション設定などはリセットされてしまいます
    丁寧にやるならlocalStorage→chrome.storage.localにデータを移行する処理を盛り込めばいいのですが……正直面倒なので拙作では手を抜きました……あしからず

ちなみに以前からそもそもbackground等でlocalStorageの使用は推奨されておらず、chrome.storageを使うようにという注意書きがどこかにあったかと思います
なお、chrome.storage APIを使うためには、manifest.jsonの"permissions"に"storage"の追加が必要です。

ちなみに、拙作拡張機能は該当しませんでしたが、background(Service Worker)ではsetTimeout()やsetInterval()等も使えないことにご注意を……代わりにchrome.alarms APIを使います("permissions"に"alarms"の追加が必要です)。
なお、こちらはV2でも"persistent": falseなbackgroundだと使えなかった気がします

オプション画面(options_ui)でbackgroundの関数が呼び出せなくなった!

Manifest V2だと、chrome.extension.getBackgroundPage()によりbackgroundのページ(windowオブジェクト)を取得することができ、その下の関数を呼び出したりもできたのですが、V3でbackgroundがService Workerに変わったことによりこれができなくなりました(chrome.extension.getBackgroundPage()でundifinedが返される)。
options_uiとbackgroundとの間でやり取りを行いたい場合には、

  • 機能を呼び出したい場合、chrome.runtime.sendMessageを使う
  • データを共有したい場合、chrome.storage APIを使う

のように、content_scriptsの場合と同じような仕組みに書き換える必要があると思われます(他によい方法があれば教えてください)。

HTTPヘッダの書き換えなどはどうなるの?(webRequestBlockingからdeclarativeNetRequestへ)

Manifest V3では、"webRequestBlocking"(ブロッキング機能付きのWeb Request API)の機能が廃止され、"declarativeNetRequest"(宣言的なNet Request API)へと置き換えられています
なお、"webRequest"の方はV3でも存在しており、例えばネットワークトラフィックをモニタする等の用途でならば使用できます

ざっくりというと、今まではbackground(JavaScript)内でHTTP Request HeadersやResponse Headersを任意の条件で動的に書き換えたりできていたものが(chrome.webRequest.onBeforeSendHeaders.addListener/chrome.webRequest.onHeadersReceived.addListener)、予め宣言された定義に基づいての書き換えとなる、ということですかね。

定義方法には静的なものと動的なものがあります。
詳細は公式ページを参照してください

NetRequestの静的な定義方法

manifest.json内で、"permissions"に"declarativeNetRequest"を追加の上で、

"declarative_net_request": {
    "rule_resources": [
        {
            "id": "ruleset",
            "enabled": true,
            "path": "ruleset.json"
        }
    ]
}

のように、ルールを記述したJSON(例ではruleset.json)のパスを指定しておき、そのJSON内でルールを定義する方法です。
ルールの定義例はこちら

NetRequestの動的な定義方法

manifest.json内では、"permissions"に"declarativeNetRequest"を追加するだけにとどめ、background(Service Worker)のソースコード内にて、ルールを定義したオブジェクトをchrome.declarativeNetRequest.updateDynamicRules()にて追加/削除する方法です。
具体例はこちら
なお、動的に追加された既存のルール(拡張機能の更新前に登録されたものも含む)に同じidのものが存在したりすると登録に失敗するようなので、追加したいルールと同じidをremoveRuleIds で指定しておくのが無難なようです(よりよい方法があれば教えてください)

Service Workerのエラーが確認できない!?

Manifest V3にしたら(backgroundがService Worker)、拡張機能のページ(chrome://extensions)で[エラー]となっているのに、これをクリックしても正しく表示されないし、「Service Worker (無効)」を開こうとしても開けない(ついでにいえば単に「(無効)」と出ているだけなので、未使用時にサスペンドされているだけなのか、エラーにより起動されないのか区別がつかない)という現象がたまに発生します。

どうも、Service Workerの起動(初期化)時にエラーが発生した場合などでこうなってしまうようです。
もっとも、「Service worker registration failed. 」のようにきちんとエラーが表示されることもあるので、いまひとつ発生条件がよくわかりませんが……

これを回避してエラー内容を確認するためには、

といった対処方法があるようです。

自分はとりあえず後者でしのいでいます。
[manifest.json]

"background" : {
    "service_worker" : "background-wrapper.js"
}

[background-wrapper.js]

try {
    importScripts('scripts/background.js');
}
catch (error) {
    console.log(error);
}

「Wrapperはmanifest.jsonと同じ階層に置く必要がある」とのことですが、それ以外の階層においたときにどうなるのかは未検証です(例えば"scripts/background-wrapper.js"のように違う階層においても、importScripts()するときのパスにさえ気をつければ通常の動作では問題無さそうではあるんですが)

onRuleMatchedDebugはデベロッパーモードによる開発時にのみ有効(Chrome ウェブストアからインストールすると動かない件)

Twitter メディアダウンローダのManifest V3対応版Chrome ウェブストア等にアップロードしたところ、なぜかそこからインストール(更新)した拡張機能が動作しない、という罠にはまりました

原因は、デバッグ用に設定していたchrome.declarativeNetRequest.onRuleMatchedDebug.addListener()が、実はローカルで[パッケージ化されていない拡張機能を読み込む]により読み込んだ拡張機能でしか機能しない(Chrome ウェブストアからインストールした場合にはonRuleMatchedDebugはundefinedになってしまう)というものでした。
その時点でエラーになることでService Workerの実行が中断されて拡張機能が動作しない&wrapper設定前だったのでエラーの確認もできないという状態

これ、たしかに公式ページにも

Fired when a rule is matched with a request. Only available for unpacked extensions with the declarativeNetRequestFeedback permission as this is intended to be used for debugging purposes only.

https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/#event-onRuleMatchedDebug

としっかり書かれているので、確認しなかった自分が悪いんですよ、ね……。

実運用としては、

のがよいかと思われます。

manifest.json上の"permissions"と、インストール時に確認される権限の関係について

Manifest V3に対応した拡張機能だと、更新時に無効化されて、再度有効化しようとすると追加で権限の確認をされることもあるかと思います。
今回自分が遭遇したパターンだと、

permissions(manifest.json) 権限(拡張機能) Permissions(Extension)
declarativeNetRequest 任意のページのコンテンツをブロック Block content on any page
declarativeNetRequestFeedback 閲覧履歴の読み取り Read your browsing history

が該当しました。
これ、V2のときにwebRequest/webRequestBlockingで実現していたのと同じようなことをやっているだけであっても、V2のときだと拡張機能のインストール時や更新時には権限として特になにも表示されなかったのに、declarativeNetRequest*にした途端、表示(権限の確認)がされるようになってしまいます。

まぁ、それだけセキュリティ的に厳しくなったということなんですが……正直、

  • 最初のインストール時→ほとんどの人が警告の中身を見ないでOKしてるんじゃないの?
  • 更新時→追加で権限を求める警告がでるので、そこで初めて気づくユーザーも多いと思うんだけど……これ、いたずらに不安を煽るだけになってない?
    更新したことで表示された=悪意がある機能が追加された、と思われて開発者に対する悪印象も植えつけられてしまう可能性

とも思えてしまい、どうにもモニョる……というのはここだけの話です……。
かといってより適切な注意喚起の仕方を思いつくわけでもないのが辛いところ……

その他(覚書)

拙作拡張機能では未対応もしくは対応する必要がなかったポイントについて覚書程度にメモしておきます。

Service Workerは「無効」時には破棄されてしまう

しばらく動いていないService Workerは「無効」化状態になります(破棄されます)。このとき、グローバル変数等も初期化されてしまうために、消えてほしくないデータに関してはchrome.storage APIを使うなりして保持しておく必要がある模様です。
拙作拡張機能は現時点(2022年10月初旬)で一部未対応(オプション変更時に把握可能なTwitterのページなどをリロードさせる処理など)

CSP(Content security policy)についても変更がある

CSP(Content security policy)のmanifest.jsonでの指定方法等も変更になっており、またsandbox用のオプションが追加されているようです
幸いにして(?)拙作拡張機能ではCSPを指定したものは無かったので、深く調べること無くスルーしてしまっています💦

リモートコードの制限

Manifest V2では可能であった、リモートでホストされている(拡張機能のパッケージに含まれていない)ソースコードを読み込んでの実行はできなくなっています
これはTampermonkey等のユーザースクリプトマネージャーにとっては致命的な制限となりえます。
どうやらGoogle側も対応予定ではあるようですが、2022年10月初旬時点ではどうなるのか不透明です。
単に自分が認識できていないだけですでに対応されているのかも知れませんが……

任意コード実行の制限

Manifest V2では可能であった、任意のコード文字列の埋め込み(実行)も、Manifest V3では制限が厳しくなっています
具体的にはmanifest.jsonの"permissions"に"scripting" 権限を追加した上で、chrome.scripting APIの仕様に従って、staticなファイルや任意のfunctionの挿入を行う必要があるようです。