株式会社クリアコード > ククログ

ククログ

タグ:

Active Directoryを使用していない環境で、Firefoxに任意のルート証明書を自動インポートする方法

既報の通り、Firefox 52以降のバージョンではActive Directoryのグループポリシー経由で配布された証明書を自動的にインポートできるようになっています。アドオンなどによる証明書の自動インポートができなくなったため、現時点ではFirefoxに任意のルート証明書を組み込んだ状態にするには、これが唯一の方法となっています。

ただ、Active Directoryを運用してない小規模の組織や、検証用のスタンドアロンのPCでは、何らかの方法で「Active Directoryのグループポリシー経由で証明書を配布した状態」と同じ状態にする必要があります。以下はそのための作業手順の説明となります。

背景

Windowsの証明書データベースは、実際にはレジストリ上の複数の位置にある情報が集約されて認識された物です。証明書のバイナリはレジストリ上ではBlob形式のレジストリ値として保存されています。

この事は、任意の証明書を主導でインポートした後でレジストリエディタを起動し HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates 以下の状態を見ることで分かります。

Active Directoryのグループポリシーで証明書を展開すると、証明書データベースを構成する複数のレジストリキーのうち、以下の位置のいずれかに情報が書き込まれます。

  • HKEY_LOCAL_MACHINE\Software\Microsoft\SystemCertificates\Root\Certificates
  • HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates
  • HKEY_LOCAL_MACHINE\Software\Microsoft\EnterpriseCertificates\Root\Certificates

よって、「Active Directoryのグループポリシー経由で証明書を配布した状態」と同じ状態を作り出すためには、必要な証明書の情報を上記のキー配下に書き込めばよいと言う事になります。

準備

以上のような背景があるため、Firefoxにインポートさせたい証明書はまず一旦、Windowsの証明書データベースにインポートしておく必要があります。まだインポートされていない場合は、以下の手順で証明書をインポートしておいて下さい。

  1. 証明書ファイルをダブルクリックして、証明書のプロパティを開く。
  2. 「詳細」タブでフィールド「拇印」の値(SHA-256ではなくSHA-1)を確認し、控えておく。
    以下、拇印は49cb1e088952d5116738db438a11c5aba7a12e01と仮定する。
  3. 「全般」タブで「証明書のインストール」ボタンをクリックする。
  4. 証明書のインポートウィザードが開かれるので、以下の設定でインポートする。
    • 現在のユーザー 用
    • 「証明書を全て次のストアに配置する」で「エンタープライズの信頼」を選択

レジストリファイルの作成

レジストリエディタでインポートするための証明書ファイルは、以下の手順で作成します。

  1. 「ファイル名を指定して実行」で regedit.exe と入力し、レジストリエディタを起動する。
  2. 「編集」→「検索」で、検索する値として、当該証明書の拇印(SHA-1 Fingerprint)を入力する。
    ここでは49cb1e088952d5116738db438a11c5aba7a12e01と仮定する。
  3. 名前が上記の拇印と一致する*1キーが見つかる。 (「準備」の項の手順でインストールした場合、 HKEY_CURRENT_USER\Software\Microsoft\SystemCertificates\trust\Certificates配下にある。)
  4. 見つかったキーを右クリックし、「エクスポート」を選択してEnterpriseCert.regなどの名前で保存する。
  5. 「準備」の項の手順でインストールした場合、3で見つかったキーをレジストリエディタ上で削除する。
  6. 4でエクスポートしたファイルをテキストエディタで開く。
    (ファイルのエンコーディングは「UTF-16LE(BOM有り)」となっている)
  7. ファイルの中に含まれる文字列HKEY_〜\拇印となっている箇所(上記の例であればHKEY_CURRENT_USER\Software\Microsoft\SystemCertificates\trust\Certificates\49CB1E088952D5116738DB438A11C5ABA7A12E01)をすべて以下の文字列に置換する。
    HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates\拇印
    (「準備」の項の手順でインストールした場合、HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates\49CB1E088952D5116738DB438A11C5ABA7A12E01。)
  8. 編集したファイル「EnterpriseCert.reg」を上書き保存する。

以上の手順で作成したレジストリファイル「EnterpriseCert.reg」を各クライアントPCでインポートすると、グループポリシーで証明書を配布したのと同じ状態になります。

まとめ

Windowsのレジストリを編集して、Active Directoryによるグループポリシーの配布ができない環境で同等の結果を得て、Firefoxのルート証明書自動インポート機能の動作を確認する手順をご説明しました。

*1 大文字小文字は区別されません。

タグ: Mozilla
2018-05-18

WebExtensionsによるFirefox用の拡張機能で、キーボードショートカットの変更用UIを提供するライブラリ:ShortcutCustomizeUIMenuUI.js

(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物です。)

従来型のアドオンでキーボードショートカットを実現する方法には、XULの<key>要素を使う方法と、JavaScriptでキーイベントを捕捉する方法の2通りがありました。WebExtensionsでは、前者はmanifest.jsoncommandsでショートカットを定義する方法、後者はcontent scriptで実装する方法がそれぞれ対応します。ブラウザウィンドウのどこにフォーカスがあっても動作するキーボードショートカットを実現する方法としては、前者の方法が唯一の選択肢となります*1

Firefox 60以降のバージョンではmanifest.jsonで定義されたキーボードショートカットを後から任意の物に変更できるようになったのですが、Google Chromeでは拡張機能が定義したキーボードショートカットを横断して制御できる設定画面をChrome自体が提供しているのに対し、Firefox 60にはまだその機能がありません。実際にキーボードショートカットを変更するためには、commands.updateという機能を使って各アドオンが自前で設定UIを提供する必要があります。

そこで、Firefox本体で設定UIが提供されるようになるまでのつなぎとして、キーボードショートカットの変更のためのUIを提供する軽量なライブラリ ShortcutCustomizeUI.jsを開発しました。

使い方

このライブラリは、キーボードショートカットの変更用UIとして機能するHTMLのコード片をDocumentFragmentとして生成する物です。使い方としては、まず設定画面を提供するHTMLファイルにShortcutCustomizeUI.jsを読み込ませます。

<script type="application/javascript" src="./ShortcutCustomizeUI.js"></script>

このファイルを読み込むとShortcutCustomizeUIというオブジェクトが利用可能になります。ShortcutCustomizeUI.build()を実行するとPromiseが返され、そのPromiseの解決後の値として、生成されたDocumentFragmentが得られます。後は、以下のようにして設定画面の中にDocumentFragmentを埋め込むだけです。

ShortcutCustomizeUI.build().then(list => {
  document.getElementById('shortcuts').appendChild(list);
});

すると、以下のスクリーンショットのようなUIが使えるようになります。 (実際に表示されたUIのスクリーンショット) 各ショートカットにはコマンドの名前もしくは説明文がラベルとして表示され、行の右端のボタンをクリックすることで初期値に戻すこともできます。

  • このライブラリはmanifest.jsoncommandsで定義されている全てのコマンドを自動的に走査し、UIに列挙します。コマンド名を各言語に応じた翻訳で表示したい場合は、manifest.json自体の国際化のための機能を使用して下さい。
  • ShortcutCustomizeUI.build()に指定できるオプションの詳細については、リポジトリ内のREADMEファイルを参照して下さい。
  • 実際にはショートカットとしては使えないキーの組み合わせも登録できる場合がありますが、その場合、そのショートカットは当然ですが機能しません。

Firefoxの組み込みのキーボードショートカットと衝突するショートカットを設定した場合、Firefoxの機能の方が優先的に動作します。アドオン側のショートカットを優先することは、現時点ではできません(1325692 - [commands] Explicit support for overriding built-in keyboard shortcuts by WebExtensionsも参照して下さい)。

改善のためのご協力のお願い

このアドオンが生成するUIにはキーボードのモディファイアキーやその他のキー名が表示されますが、キーの表示名は言語によって異なる場合があります。そのため、ライブラリ内で言語ごとの表示名を定義していますが、現状ではごく一部の言語のみの対応に留まっています。

もし他の言語のことに詳しい方がいらっしゃいましたら、キーの表示名と内部名の対応表の追加にご協力をいただければ幸いです。

まとめ

WebExetnsionsによるFirefox用アドオンに簡単にキーボードショートカット変更用のUIを追加できる軽量ライブラリであるShortcutCustomizeUI.jsについて、その使い方を解説しました。「以後確認しない」のようなチェックボックスを伴った確認ダイアログを表示するRichConfirm.jsや、メニュー風のUIを提供するMenuUI.jsと併せて、Firefox用アドオンの開発にご活用いただければ幸いです。

*1 使用できるキーの種類などの面で自由度が高いのは後者ですが、実行のためには全てのページでユーザースクリプトを実行する権限(<all_urls>)が必要な上に、コンテンツ領域にフォーカスがある時でないとイベントを認識できない、addons.mozilla.orgのページやabout:addonsなどのFirefox自体が提供するページでは動作しない、などの欠点があります。

タグ: Mozilla
2018-05-14

Mozilla Firefox ESR60でのPolicy Engineによるポリシー設定の方法と設定項目のまとめ

FirefoxはESR60以降のバージョンにおいて、法人利用者向けに設定を集中管理する新しい仕組みである「Policy Engine」が導入・有効化されています。Policy Engineで可能な設定の一部は従来のMCD(AutoConfig)と重複していますが、Policy Engineには従来型アドオンによるカスタマイズの代替手段としての性質もあり、MCDではできなかった設定も存在しています。

過去の記事(第一報グループポリシーについての続報)において、Firefox 60のベータ版を対象として設定の手順やその時点で使用できるようになっていた設定項目を紹介してきましたが、この度のFirefox ESR60正式版のリリースに伴い、改めて最新の状況について情報を整理します。

ポリシー設定を反映する2つの方法

WindowsでPolicy Engineを使って設定を集中管理する方法には、以下の2通りがあります。

  • ポリシー定義ファイル(policies.json)を使う
  • Active Directoryのグループポリシーを使う(Windowsのみ)
policies.jsonを使う

この方法を使う場合には、各クライアントPC上のFirefoxのインストール先フォルダ(C:\Program Files\Mozilla FirefoxC:\Program Files (x86)\Mozilla Firefox など)配下にdistributionという名前でフォルダを作成し、さらにその中にpolicies.jsonという名前で以下の内容のテキストファイル(JSON形式)を設置します。

{
  "policies": {
    (ここにポリシー設定を記述する)
  }
}

例えば、後述の"BlockAboutAddons""BlockAboutConfig"を両方とも設定する場合は以下の要領です。

{
  "policies": {
    "BlockAboutAddons": true,
    "BlockAboutConfig": true
  }
}

この方法の利点は、ファイルを配置する事さえできればLinux版およびmacOS版(ファイルはFirefox.appをディレクトリと見なして、その配下に設置する事になります)においても使用できるという点です。

欠点は、設定ファイルを各クライアントに適切に配置しなくてはならないという事と、この方法で設定しただけでは充分に機能しない設定があるという事です。一部の設定項目は、期待通りの結果を得るためには後述のグループポリシーで設定する必要があります。

本記事では、各設定項目の指定はこちらの方法で設定する想定で解説します。

Active Directoryのグループポリシーを使う

Windows版Firefoxでは、Active Directoryのグループポリシーを使ってPolicy Engineの設定を制御することができます。Mozillaの外部プロジェクトとして公開されているポリシーテンプレート*1をダウンロードして展開し、ドメインコントローラの C:\Windows\SYSVOL\domain\Policies\PolicyDefinitions 配下に各ファイルを配置すると、グループポリシーの管理画面上で「コンピューターの構成」および「ユーザーの構成」の「管理用テンプレート」配下にFirefox向けの設定項目が表示されるようになります。各設定項目の働きは、policies.jsonで設定可能な項目に対応しています。

Firefoxの設計上は、同じポリシーがユーザー向けとコンピューター向けの両方に設定されていた場合、ユーザーに設定されたポリシーよりもコンピューターに設定されたポリシーの方が優先的に反映されます。例えば「Block About Config」というポリシーについて、ユーザー向けのポリシーで「有効」と設定していても、コンピューターのポリシーで「無効」と設定していると、このポリシーは「無効」と扱われます。基本的にはコンピューター向けのポリシーのみ設定して運用するとよいでしょう。

なお、2018年5月14日時点では、ポリシーテンプレートはWindows Server 2008以降で使用できるadmx形式のファイルのみが提供されています*2。Windows 2003 ServerをドメインコントローラとしてActive Directoryを運用している場合には、上記のポリシーテンプレートを参考にadm形式のポリシーテンプレートを作成する必要があります。ポリシーテンプレートを用意できない場合は、plicies.jsonを各クライアントに設置する方法を取らなくてはなりません。

この方法の利点は、ドメインコントローラ上での操作のみで管理を完結できるという事です。また、こちらの方法で設定した場合には、全ての設定項目が期待通りに機能するというメリットもあります。

欠点は、当然ですが、Active Directoryでドメインを運用中でないと利用できないという点です。ドメインを構築していないような小規模の組織においては、policies.jsonを使う他ないという事になります。

Firefox ESR60.0で使用できる設定項目

Policy Engineで設定可能な項目はいくつかのカテゴリに分類できます。以下、カテゴリごとにご紹介します。

シングルサインオンを許可するための設定

シングルサインオンに関する設定としては、以下の設定が可能です。 設定は全て"Authentication"配下に定義します。

{
  "policies": {
    ...
    "Authentication": {
      // SPNEGO認証を許可するサイトのリスト。
      // (MCDでの network.negotiate-auth.trusted-uris に相当)
      "SPNEGO": ["www.example.com", "http://www.example.org/", ...],

      // 認証情報の委任を許可するサイトのリスト。
      // (MCDでの network.negotiate-auth.delegation-uris に相当)
      "Delegated": ["www.example.com", "http://www.example.org/", ...],

      // NTLM認証を許可するサイトのリスト。
      // (MCDでの network.automatic-ntlm-auth.trusted-uris に相当)
      "NTLM": ["www.example.com", "http://www.example.org/", ...]
    },
    ...
  }
}

"SPNEGO""Delegated"、および"NTLM"の各設定は、必要な物だけを設定します。不要な設定は省略して構いません。

about: で始まるURIのページへのアクセスを禁止

URIがabout: で始まる特別なページのうちいくつかは、以下の設定でアクセスを禁止する事ができます。

{
  "policies": {
    ...
    // about:addons(アドオンマネージャ)の表示を禁止する。
    // アドオンの設定変更、有効・無効の切り替え、インストール、
    // アンインストールなどの操作を全面的に禁止することになる。
    "BlockAboutAddons": true,

    // about:config(設定エディタ)の表示を禁止する。
    // 設定画面に現れない詳細な設定の変更を禁止することになる。
    "BlockAboutConfig": true,

    // about:profiles(プロファイル管理画面)の表示を禁止する。
    // プロファイルを指定しての起動、アドオンを無効にしての起動などを
    // 禁止することになる。
    "BlockAboutProfiles": true,

    // about:support(トラブルシューティング情報)の表示を禁止する。
    // アドオンを無効にしての起動などを禁止することになる。
    "BlockAboutSupport": true,
    ...
  }
}
初期ブックマーク項目の作成

Policy Engineを使うと、初期状態で任意のブックマーク項目やフォルダを登録済みの状態にする事ができます。

{
  "policies": {
    ...
   // 初期状態で登録しておく追加のブックマーク項目の定義。任意の個数登録可能。
   "Bookmarks": [
     { // ブックマークツールバーにブックマークを作成する場合
       "Title":     "example.com",
       "URL":       "http://www.example.com/",
       "Favicon":   "http://www.example.com/favicon.ico",
       "Placement": "toolbar"
     },

     { // ブックマークツールバーの中にフォルダを作って、その中にブックマークを作成する場合
       "Title":     "example.com",
       "URL":       "http://www.example.com/",
       "Favicon":   "http://www.example.com/favicon.ico",
       "Placement": "toolbar",
       "Folder":    "ツールバー初期項目"
     },

     { // ブックマークメニューにブックマークを作成する場合
       "Title":     "example.org",
       "URL":       "http://www.example.org/",
       "Favicon":   "http://www.example.org/favicon.ico",
       "Placement": "menu"
     },

     { // ブックマークメニューの中にフォルダを作って、その中にブックマークを作成する場合
       "Title":     "example.org",
       "URL":       "http://www.example.org/",
       "Favicon":   "http://www.example.org/favicon.ico",
       "Placement": "menu",
       "Folder":    "メニュー初期項目"
     },
     ...
   ],

   // Firefox自体の初期ブックマーク項目を作成しない。
   // (「よく見るページ」などのスマートブックマーク項目を除く)
   // この設定は初回起動時・新規プロファイル作成時にのみ反映され、
   // 既に運用中のプロファイルには反映されない(作成済みの初期ブックマーク項目は削除されない)。
   "NoDefaultBookmarks": true,
    ...
  }
}
証明書のインポート

証明書データベースからのルート証明書の自動インポートに関いては、以下の設定が可能です。 設定は全て"Certificates"配下に定義します。

{
  "policies": {
    ...
    "Certificates": {
      // 管理者によってシステムに配布されたルート証明書をFirefoxの証明書データベースにインポートする。
      // (Windowsであれば、Active Directoryのグループポリシーで配布されたルート証明書をインポートする)
      "ImportEnterpriseRoots": true
    },
    ...
  }
}

この設定による自動インポートは、Windowsのレジストリ上の特定の位置に存在するルート証明書(Active Directoryのグループポリシーで配布された証明書)のみが対象となります。 ユーザーが自分の権限で証明書データベースに登録したルート証明書はインポートされません。

Cookieの保存の可否と有効期限

Cookieに関する設定は、以下の物があります。 設定は全て"Cookies"配下に定義します。

{
  "policies": {
    ...
    "Cookies": {
      // 指定のWebサイト(オリジンで指定)でCookie、IndexedDB、
      // Web Storage、およびService Worker用Cacheを保存する。
      // (任意に無効化はできない)
      "Allow": ["http://example.com", "https://example.org:8080", ...],

      // 指定のWebサイト(オリジンで指定)でCookie、IndexedDB、
      // Web Storage、およびService Worker用Cacheを保存する。
      // (任意に無効化はできない)
      // また、これらのホストに保存済みのCookieがあった場合、それらは削除される。
      "Block": ["http://example.com", "https://example.org:8080", ...],

      // サードパーティCookieかどうかに関わらず、全てのCookieを一律で拒否する。
      "Default": false,

      // サードパーティによるCookieの受け入れ可否。
      // 以下の3つの中からいずれかを指定する。
      //  * "always":       全て受け入れる。
      //  * "never":        全く受け入れない。
      //  * "from-visited": 既に保存済みのサードパーティCookieがある場合のみ受け入れる。
      // (MCDでの network.cookie.cookieBehavior に相当)
      "AcceptThirdParty": "always",

      // 全てのCookieについて「Firefoxを終了するまでを有効期限とし、
      // Webサイトが指定した保持期限を無視する。
      // (MCDでの network.cookie.lifetimePolicy に相当)
      "ExpireAtSessionEnd": true,

      // Cookieに関する設定のユーザーによる変更を禁止する。
      "Locked": true
    },
    ...
  }
}
様々な機能の無効化

機能の無効化に関する設定には、以下の項目があります。

{
  "policies": {
    ...
    // Firefoxの自動更新を禁止する。
    "DisableAppUpdate": true,

    // PDF.jsを無効化する。
    "DisableBuiltinPDFViewer": true,

    // 開発ツールを無効化する。
    // メニュー項目、キーボードショートカットの両方とも無効化する。
    // (MCDでの devtools.policy.disabled および
    //   devtools.chrome.enabled に相当)
    "DisableDeveloperTools": true,

    // フィードバックの送信、詐欺サイトの誤検出の報告を禁止する。
    "DisableFeedbackCommands": true,

    // FirefoxアカウントとSyncを無効化する。
    "DisableFirefoxAccounts": true,

    // Firefox Screenshotsを無効化する。
    "DisableFirefoxScreenshots": true,

    // Firefoxの新機能のテストへの参加( https://support.mozilla.org/ja/kb/shield )を禁止する。
    "DisableFirefoxStudies": true,

    // 「忘れる」ボタンを無効化する。
    "DisableForgetButton": true,

    // フォームの入力履歴の保存とオートコンプリートを無効化する。
    // (MCDでの browser.formfill.enable に相当)
    "DisableFormHistory": true,

    // マスターパスワードの設定を禁止する。
    "DisableMasterPasswordCreation": true,

    // Pocketを無効化する。
    // (MCDでの extensions.pocket.enabled に相当)
    "DisablePocket": true,

    // プライベートブラウジング機能の使用を禁止する。
    "DisablePrivateBrowsing": true,

    // 他のブラウザからのブックマークや設定のインポート機能を無効化する。
    // (ただし、ESR60時点では初回起動時の設定インポートは無効化されない)
    "DisableProfileImport": true,

    // about:support(トラブルシューティング情報)からのプロファイルのリフレッシュ操作(データの消去)を禁止する。
    "DisableProfileRefresh": true,

    // クラッシュからの復帰時と自動更新の時以外で、
    // セーフモードでの起動(アドオンを無効化しての起動)を禁止する。
    // ただし、コマンドライン引数でのセーフモードまで無効化するにはグループポリシーでの設定が必要。
    "DisableSafeMode": true,

    // セキュリティ警告を敢えて無視することによる危険な操作、を完全に禁止する。
    "DisableSecurityBypass": {
      // 証明書の警告の例外を登録するボタンを隠して、
      // 例外を絶対に登録できないようにする。
      // (MCDでの security.certerror.hideAddException に相当)
      "InvalidCertificate": true,

      // 詐欺サイト等の警告における「危険性を無視」リンクを隠して
      // コンテンツを絶対に表示できないようにする。
      // (MCDでの browser.safebrowsing.allowOverride=false に相当)
      "SafeBrowsing": true
    },

    // 画像をコンテキストメニューからデスクトップの壁紙に設定することを禁止する。
    "DisableSetDesktopBackground": true,

    // システムアドオンの更新を禁止する。
    "DisableSystemAddonUpdate": true,

    // 統計情報の収集・送信を禁止する。
    // (MCDでの datareporting.healthreport.uploadEnabled および
    //   datareporting.policy.dataSubmissionEnabled に相当)
    "DisableTelemetry": true,

    // 起動時に既定のブラウザにするかどうかを確認しない。
    // (MCDでの browser.shell.checkDefaultBrowser に相当)
    // 既定のブラウザに設定できなくする設定、ではないことに注意。
    "DontCheckDefaultBrowser": true,

    // パスワードマネージャによるパスワードの保存を禁止する。
    // (MCDでの signon.rememberSignons に相当)
    "OfferToSaveLogins": false,
    ...
  }
}
UIの初期状態

FirefoxのUIの初期状態を変更する設定としては、以下の項目があります。 いずれも、ユーザーが任意に状態を変更した場合はそちらが優先されます。 (強制的にその状態に固定する、ということはできません。)

{
  "policies": {
    ...
    // 初期状態でブックマークツールバーを表示する。
    "DisplayBookmarksToolbar": true,

    // 初期状態でメニューバーを表示する。
    "DisplayMenuBar": true,

    // 検索ツールバーをロケーションバーと統合するかどうか。
    //  * "unified":  統合する(Firefox ESR60以降の初期状態)
    //  * "separate": 分離する(Firefox ESR52以前の初期状態)
    "SearchBar": "separate",
    ...
  }
}
プライバシー情報の取り扱いに関わる設定

プライバシー情報に関する設定は、以下の項目があります。 トラッキング防止機能の設定は"EnableTrackingProtection"配下に定義します。

{
  "policies": {
    ...
    "EnableTrackingProtection": {
      // トラッキング防止機能の有効無効。
      //  * true:  有効(トラッキングを防止する)
      //  * false: 無効(トラッキングを許容する)
      // (MCDでの privacy.trackingprotection.enabled および
      //   privacy.trackingprotection.pbmode.enabled に相当)
      "Value": false,

      // トラッキング防止機能の、ユーザーによる設定変更の可否。
      //  * true:  ユーザーの設定変更を禁止(設定を固定する)
      //  * false: ユーザーの設定変更を許可
      "Locked": true
    },

    // Firefox終了時に、その都度履歴やCookieなどのプライバシー情報を全て消去する。
    // (MCDでの privacy.sanitize.sanitizeOnShutdown に相当)
    "SanitizeOnShutdown": true,
    ...
}
アドオンの制御

アドオンの制御に関する設定には、以下の項目があります。 管理者によるアドオンのインストールやアンインストールの制御は"Extensions"配下に、ユーザーによるインストールの可否に関する設定は"InstallAddonsPermission"配下に定義します。

{
  "policies": {
    ...
    "Extensions": {
      // 自動的にインストールするアドオンのXPIファイルの位置。
      // ("InstallAddonsPermission"配下の"Default"がfalseであってもインストールされる)
      "Install": [
        // URLでの指定
        "https://example.com/my-addon.xpi",
        // ファイルパスでの指定
        "C:\\Path\\To\\XPI\\File\\my-another-addon.xpi",
        ...
      ],

      // インストールされていた場合に自動的にアンインストールするアドオンのID。
      "Uninstall": [
        "my-legacy-addon@example.com",
        "my-obsolete-addon@example.org",
        ...
      ],

      // アンインストール、無効化を禁止するアドオンのID。
      // (設定の変更は可能)
      "Locked": [
        "my-security-addon@example.com",
        "my-privacy-addon@example.org",
        ...
      ]
    },

    // アドオンのインストールの可否に関する制御。
    "InstallAddonsPermission": {
      // アドオンのインストール時に警告を行わないページ。
      // (オリジンで指定、`https`のみ有効)
      "Allow": ["https://example.com", "https://example.org:8080", ...],

      // ユーザーによるアドオンのインストールを禁止する。
      // (MCDでの xpinstall.enabled に相当)
      "Default": false
    },
    ...
}

"Extensions""Install"で指定したアドオンは、実行時のユーザープロファイル内にインストールされます。"Extensions""Uninstall"も、ユーザープロファイル内にあるアドオンが対象となります。

すべてのユーザーで常に特定のアドオンがインストールされた状態としたい場合は、特定のフォルダにファイルを設置することによるサイドローディングでインストールする必要があります。

Adobe Flashプラグインの制御

Flashプラグインの制御については、以下の項目があります。 設定は全て"FlashPlugin"配下に定義します。

{
  "policies": {
    ...
    "FlashPlugin": {
      // 指定のWebサイト(オリジンで指定)でFlashを
      // Click to Play無しで自動実行する。
      // (任意に無効化はできない)
      "Allow": ["http://example.com", "https://example.org:8080"],

      // 指定のWebサイト(オリジンで指定)でFlashの実行を禁止する。
      // (任意に無効化はできない)
      "Block": ["http://example.com", "https://example.org:8080"]
    },
    ...
}
ホームページの設定

起動時に表示されるページの設定には、以下の項目があります。 ホームページに関する設定は全て"Homepage"配下に定義します。

{
  "policies": {
    ...
    "Homepage": {
      // ホームページのURI(複数タブを設定する場合は最初のタブの内容)
      // (MCDでの browser.startup.homepage および
      //   browser.startup.page=1 に対応)
      "URL": "http://example.com",

      // ホームページを複数設定する場合の、2番目以降のタブの内容
      "Additional": ["https://example.org:8080", ...],

      // ホームページの変更を禁止する
      "Locked": true
    },

    // 起動後に1回だけ表示されるページのURL。
    "OverrideFirstRunPage": "http://www.example.com/",
    ...
}
ポップアップブロックの制御

ポップアップブロック機能の制御に関しては、以下の設定項目があります。 設定は全て"PopupBlocking"配下に定義します。

{
  "policies": {
    ...
    "PopupBlocking": {
      // window.open() によるポップアップを確認なしで許可するWebサイト。
      // オリジンで指定し、任意に無効化はできない。
      "Allow": ["http://example.com", "https://example.org:8080"],

      // ポップアップブロック機能を初期状態で無効化する。
      // (MCDでの dom.disable_open_during_load に相当)
      "Default": false,

      // ユーザーによる設定の変更を禁止する。
      "Locked": true
    },
    ...
}
プロキシの設定

プロキシに関する設定は以下の項目があります。 設定は全て"Proxy"配下に定義します。

{
  "policies": {
    ...
    "Proxy": {
      // プロキシを使用するかどうか。
      //  * "none":       プロキシを使用しない。
      //  * "system":     システムで設定されたプロキシ設定に従う。
      //                  (Windowsであれば「インターネットオプション」の設定)
      //  * "autoDetect": 接続しているネットワークのDHCPで通知されたWPADの設定に従う。
      //  * "autoConfig"  以降の項目で指定されたPACファイルを使用する。
      //  * "manual":     以降の項目での個別の設定に従う。
      // (MCDでの network.proxy.type に相当)
      "Mode": "none",

      // "Mode":"autoConfig"の時に使用するPACファイルの取得先URL。
      // (MCDでの network.proxy.autoconfig_url に相当)
      "AutoConfigURL": "file://///fileserver/proxyl.pac",

      // HTTPで使用するプロキシのホストとポート番号。
      // (MCDでの network.proxy.http と
      //   network.proxy.http_port に相当)
      "HTTPProxy": "192.168.0.1:50080",

      // SSL/TLSで使用するプロキシのホストとポート番号。
      // (MCDでの network.proxy.ssl と
      //   network.proxy.ssl_port に相当)
      "SSLProxy": "192.168.0.1:50443",

      // FTPで使用するプロキシのホストとポート番号。
      // (MCDでの network.proxy.ftp と
      //   network.proxy.ftp_port に相当)
      "FTPProxy": "192.168.0.1:50022",

      // SOCKSで使用するプロキシのホストとポート番号。
      // (MCDでの network.proxy.socks と
      //   network.proxy.socks_port に相当)
      "SOCKSProxy": "192.168.0.1:51080",

      // SOCKSのバージョン。4または5のみ有効。
      // (MCDでの network.proxy.socks_version に相当)
      "SOCKSVersion": 4,

      // HTTPのプロキシを他の全てのプロトコルに使用する。
      // (MCDでの network.proxy.share_proxy_settings に相当)
      "UseHTTPProxyForAllProtocols": true,

      // プロキシを使用しないホスト。
      // (MCDでの network.proxy.no_proxies_on に相当)
      "Passthrough": "localhost,127.0.0.1,...",

      // DNSへの問い合わせにもプロキシを使用する。
      // (MCDでの network.proxy.socks_remote_dns に相当)
      "UseProxyForDNS": true,

      // プロキシに自動的にログインする。
      // (MCDでの signon.autologin.proxy に相当)
      "AutoLogin": true,

      // ユーザーによる設定の変更を禁止する。
      "Locked": true
    },
    ...
}
検索エンジンの設定

設定は全て"SearchEngines"配下に定義します。

{
  "policies": {
    ...
    "SearchEngines": {
      // 初期状態で検索バー用に登録しておく検索エンジンのリスト。
      "Add": [
        {
          "Name":               "検索エンジンの表示名",
          "IconURL":            "https://example.com/favicon.ico",
          "Alias":              "ex",
          "Description":        "検索エンジンの説明文",
          "Method":             "GET",
          "URLTemplate":        "https://example.com/search?q={searchTerms}",
          "SuggestURLTemplate": "https://example.com/suggest?q={searchTerms}"
        },
        ...
      ],

      // 指定した名前の検索エンジンを規定の検索エンジンにする。
      "Default": "Google",

      // 検索エンジンの追加を禁止する。
      "PreventInstalls": true
    },
    ...
}

検索エンジンはESR60時点では、検索語句の文字エンコーディングがCP1252で、HTTP MethodがGETである物のみ使用可能です。検索語句をUTF-8などの他のエンコーディングで指定しなくてはならない検索エンジンでは、残念ながら日本語の検索クエリは文字化けしてしまい検索できません。

"Alias"は、検索エンジンに設定するキーワードです。キーワードが設定されている検索エンジンは、ロケーションバーでex 検索したい語句のようにキーワードに続けて語句を入力する形でも呼び出せるようになります。

簡易的なアクセス制限

設定は全て"WebsiteFilter"配下に定義します。

{
  "policies": {
    ...
    "WebsiteFilter": {
      // ブロックする対象のURLを示すマッチパターン。
      // (詳細は https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns を参照)
      // ESR60時点では、http:またはhttps:で始まるサイトのみが対象で、
      // 最大1000項目まで指定可能。
      "Block": [
        "https://twitter.com/*",
        "https://*.twitter.com/*",
        ...
      ],

      // ブロック対象の例外とするURLを示すマッチパターン。
      "Exceptions": [
        "https://twitter.com/about",
        ...
      ]
    },
    ...
}

まとめ

以上、Firefox ESR60から使用可能となったPolicy Engineによるポリシー設定について、ESR60.0時点での状況を解説しました。

Policy Engine機能の実装はFirefox 59のリリース以後から急ピッチで進められたため、Firefox ESR60.0の時点では機能不足や不具合がまだまだ目立つ状況と言わざるを得ません。ただ、通常Firefoxのセキュリティアップデートはセキュリティに関わる変更のみが反映されますが、Policy EngineもESR60も法人利用者のための機能である事から、Policy Engineの不具合は、今後ESR60のセキュリティアップデートの中で修正されていく可能性もあります*3。最新の動向に注意しながら使っていきましょう。

ちなみに、ESR60時点で実装されているポリシー設定のうち、以下の機能はクリアコード在籍のエンジニアによって実装されました。 日本での法人利用の中で大きな需要があるポリシー設定については、今後も積極的に実装の提案を行っていきたいと考えています。

*1 現時点では言語リソースは英語用のみが提供されています。

*2 Windows Server 2003ドメインでもこのポリシーテンプレートで行われた設定は有効ですが、ドメインコントローラはWindows Server 2008以降のバージョンである必要があります。

*3 Policy Engineの開発を主導していた方によるPolicy Engineの紹介記事でも、ESR60のセキュリティアップデートの中でポリシーの追加や修正を行っていくことが予告されています。

タグ: Mozilla
2018-05-12

WebExtensionsによるFirefox用の拡張機能でメニューやパネル風のUIを提供するライブラリ:MenuUI.js

(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物です。)

XULでは<menupopup>という要素の中に<menuitem><menu>などの要素を置いてネイティブアプリと同じ様式の階層型メニューを実装できました。それに対し、WebExtensionsベースの拡張機能ではUIは基本的にHTMLで作成します。この時に悩み所になるのが、メニューをどうやって実装するかという点です。

HTMLにもmenu要素という物がありますが、これはXULのメニュー関連要素の完全な代替とはなりません。ボタンに対してのポップアップメニューやツールバー上のメニューは本稿執筆時点でFirefoxでは実装されておらず、唯一使用できるコンテキストメニューでの用法についても、専用のコンテキストメニューを実現する物ではなく、あくまで「Webページ上のコンテキストメニューに追加の項目を設ける」という性質の物です。

Firefoxのコンテキストメニューとは切り離された別のメニューを実現するには、今のところHTML(+JavaScript+CSS)でそれらしい見た目と振る舞いのUIを独自に実装するほかありません。実装例自体は既存の物が多数あり、React用Vue用jQuery用など、採用するフレームワークに合わせて選べます。しかし、作りたいアドオンの性質上、フレームワークの導入には気乗りしないという人もいるのではないかと思います。

そこで、このような場面で使える軽量なライブラリとしてMenuUI.jsという物を開発しました。

基本的な使い方

このライブラリは、HTMLファイル中に静的に記述された物か動的に生成された物かを問わず、<ul><li>で構成された入れ子のリストを階層型のメニューとして振る舞わせるという物です。表示例はこんな感じです。 (Tree Style Tabでの使用例)

このライブラリを使ってメニュー風UIを提供するには、まず任意のHTMLファイル内でMenuUI.jsを読み込みます。

<script type="application/javascript" src="./MenuUI.js"></script>

また、それと併せてメニューとして表示するためのリストを以下の要領で用意します。

<ul id="menu">
  <li id="menu-save">保存(&amp;S)</li>
  <li>コピー(&amp;P)
    <ul>
      <li id="menu-copy-title">タイトル(&amp;T)</li>
      <li id="menu-copy-url">&amp;URL</li>
      <li>メタデータ(&amp;M)
        <ul>
          <li id="menu-copy-author">作者名(&amp;A)</li>
          <li id="menu-copy-email">&amp;Eメール</li>
        </ul>
      </li>
    </ul>
  </li>
  <li class="separator"></li>
  <li id="menu-close">閉じる(&amp;C)</li>
</ul>
  • サブメニューを定義したい場合はリストを入れ子にして下さい。
  • この例では、後々どの項目が選択されたかを容易に判別できるようにするために、コマンドとして実行されうる項目にIDを割り当てています。(通例、サブメニューを持つ項目はそれ自体が選択されてもコマンドとしては実行されないため、それらにはIDを割り当てていません。)
  • リストの内容となるテキストがそのままメニュー項目のラベルになります。
    • この時、ラベルの一部に&(静的に記述する場合は実体参照の&amp;)を書いておくと、その次の文字がメニュー項目のアクセスキーになります。

次に、このリストをメニューとして初期化します。以下の要領でMenuUIクラスのインスタンスを作成します。

var menuUI = new MenuUI({
  root: document.getElementById('menu'), // メニューにするリストの最上位の<ul>を指定
  onCommand: (aItem, aEvent) => {
    // 項目が選択された時に実行されるコールバック。
    // ここでは項目のidで処理を振り分けるようにしている。
    switch (aItem.id) {
      case 'menu-save':
        doSave();
        return;
      case 'menu-copy-title':
        doCopy('title');
        return;
      ...
    }
  }
});

作成されたインスタンスは、open()というメソッドを実行するとメニューとして表示されます。ページ上を右クリックした際にブラウザ本来のコンテキストメニューの代わりとして表示したい場合であれば、以下のようにします。

window.addEventListener('contextmenu', aEvent => {
  aEvent.stopPropagation();
  aEvent.preventDefault();
  menuUI.open({
    left: aEvent.clientX,
    top:  aEvent.clientY
  });
});

また、何かのボタンをクリックした時にドロップダウンメニューのように表示させたい場合には、以下のようにボタンのクリック操作を監視した上で、そのボタンなどの要素をopen()メソッドの引数にanchorというプロパティで指定すると、ボタンの位置に合わせてメニューが表示されます。

const button = document.getElementById('button');
button.addEventListener('click', () => {
  menuUI.open({
    anchor: button
  });
});

コンストラクタのオプションやopen()のオプションの詳細な内容については、リポジトリ内のREADMEファイルを参照して下さい。

キーボードでの操作への対応

このライブラリの特長として、キーボード操作への対応に力を入れているという点が挙げられます。以下は、現時点で対応している代表的な操作です。

  • ↑↓カーソルキーで項目のフォーカスを移動
  • →カーソルキーでサブメニューを開く
  • ←カーソルキーでサブメニューを閉じる
  • Escキーでメニューを閉じる
  • Enterキーでフォーカスしている項目を選択(コマンドを実行)
  • メニューのラベルから検出されたアクセスキーで項目を選択(同じアクセスキーの項目が複数ある場合はフォーカス移動のみ)

メニューを開くまではポインティングデバイスを使うが、その後の操作はキーボードで行う、というような場面は意外とあります。既存のライブラリはキーボード操作に対応していなかったり、操作体系が一般的なGUIのメニューではなくWebページ上のリンクに準拠していたりと、「ネイティブアプリのGUIと同じ感覚で使える」事が特長だったXULからの移行においてストレスになる部分があったため、この点にはとりわけ気をつけて開発しています。

まとめ

XULアドオンでのカスタムメニューの代替となる軽量ライブラリであるMenuUI.jsについて、その使い方を解説しました。「以後確認しない」のようなチェックボックスを伴った確認ダイアログを表示するRichConfirm.jsと併せて、Firefox用アドオンの開発にご活用いただければ幸いです。

つづき: 2018-05-14
タグ: Mozilla
2018-05-11

Firefoxの拡張機能で「以後確認しない」のようなチェックボックスを伴った確認ダイアログを表示するには?

(この記事は、Firefoxの従来型アドオン(XULアドオン)の開発経験がある人向けに、WebExtensionsでの拡張機能開発でのノウハウを紹介する物です。)

Services.prompt.confirmEx()を代替する

Firefoxの従来型アドオンではnsIPromptServiceという内部APIを使って、「YES・NO・キャンセルの三択のダイアログ」を表示したり、「以後この確認を表示しない」のようなチェックボックスを伴った確認ダイアログを表示したりできました。

これと同等の事をWebExtensionsベースの拡張機能でやろうとすると、意外と面倒だという事実に行き当たります。

  • window.confirm()window.alert()では「以後この確認を表示しない」のようなチェックボックスを提供できない。
  • window.confirm()では「OK」と「キャンセル」の二択しかできず、三択以上の選択肢を提供できない。
  • コンテンツ領域内に埋め込む形で独自の確認ダイアログ風UIを作るのは手間がかかる。ボタンのフォーカス切り替えなどのキーボード操作にも対応するとなると、なおさら大変。
  • バックグラウンドスクリプトから任意のタイミングで確認を行うためには、コンテントスクリプトとの間での通信が必要。

そこで、nsIPromptServiceconfirmEx()select()に相当する機能を簡単に利用できるようにする軽量なライブラリとして、RichConfirm.jsという物を開発しました。 (サイドバー内に表示された確認ダイアログ風UI)

使い方は以下の通りです。

  1. RichConfirm.jsをパッケージに含める。
  2. バックグラウンドページやサイドバーなどから<script type="application/javascript" src="./RichConfirm.js"></script>のようにして読み込む。
  3. ダイアログを表示したい場面で、RichConfirm.show()またはRichConfirm.showInTab()を呼ぶ。

以下、それぞれの場合について詳しく説明します。

スクリプトを実行したページの中でダイアログを表示する

サイドバーやコンテントスクリプト内でそのページ内にダイアログを表示したい場合には、RichConfirm.show()を使います。このメソッドはオブジェクト形式で以下の引数を受け付けます。

RichConfirm.show({
  message:      '実行しますか?',   // 表示するメッセージ
  buttons:      ['はい', 'いいえ'], // 選択肢のラベルの配列
  checkMessage: '以後確認しない',   // チェックボックスのラベル
  checked:      false              // チェックボックスの初期状態
});

メソッドの戻り値はPromiseになっており、以下のいずれかの方法で結果を受け取る事ができます。

// Promiseとして使う
RichConfirm.show(...).then(result => {
  // 結果を使った処理
  console.log(result);
});

// async-awaitで使う
async confirmAndDo() {
  var result = await RichConfirm.show(...);
  // 結果を使った処理
  console.log(result);
}

Promiseが解決された結果の値は、以下のような形式のオブジェクトになっています。

{
  buttonIndex: 0,
  // 押されたボタンの番号。選択肢の配列の要素に対応し、
  // どれも選択されなかった場合は`-1`となる。
  checked: true
  // チェックボックスの状態。
}

タブの中でダイアログを表示する

バックグラウンドスクリプトから現在のタブの中にダイアログを表示したい場合には、RichConfirm.showInTab()を使います。

このメソッドを使うにはactiveTabまたはtabsの権限が必要です。 RichConfirm.show()と同じインターフェースで利用でき、戻り値の取り扱いもRichConfirm.show()と同じです。

RichConfirm.show({
  message:      '実行しますか?',   // 表示するメッセージ
  buttons:      ['はい', 'いいえ'], // 選択肢のラベルの配列
  checkMessage: '以後確認しない',   // チェックボックスのラベル
  checked:      false              // チェックボックスの初期状態
});

RichConfirm.show()には無い特長として、メソッドの第1引数としてtabs.Tabid(整数)を指定すると、現在のタブ以外の任意のタブにダイアログを表示させる事ができます。例えば、以下は現在のタブの右隣のタブにダイアログを表示する例です。

async function confirmInNextTab() {
  var [current, tabs] = await Promise.all([
    browser.tabs.getCurrent(),
    browser.tabs.query({})
  ]);
  var nextTab = tabs.filter(tab => tab.windowId == current.windowId)
                    .filter(tab => tab.index == current.index + 1)[0] || current;
  var result = await RichConfirm.show(nextTab.id, {
    message: ...
  });
  ...
}

まとめ

XULアドオンで一般的に使われていたnsIPromptServiceのconfirmEx()をの代替となる軽量ライブラリであるRichConfirm.jsについて、その使い方を解説しました。

ネイティブアプリケーションの開発に近い部分があったXULアドオンとは異なり、WebExtensionsベースでの拡張機能開発は、どちらかというとWebアプリの開発に近いと言えます。Webアプリの開発に慣れた人にとっては、古くはjQuery UIや、近代的な物ではBootstrapReactを使う方がやりやすいかもしれません。ただ、ピンポイントでconfirmEx()と同等の事がしたいだけという場合には、これらのライブラリの使い方を一から覚えるのは億劫に感じるものです。そういった場合の手軽な選択肢として、RichConfirm.jsを試してみてはいかがでしょうか。

タグ: Mozilla
2018-03-26

Firefox ESR60以降でのグループポリシーによるポリシー定義

既報の通り、Firefox ESR60以降のバージョンではPolicy Engineという新しい設定の集中管理のための仕組みが導入されます。

先の記事ではJSONファイルを使ってポリシーを定義する手順を紹介しましたが、Policy EngineはActive Directoryのグループポリシー定義にも対応しています。 Mozillaの外部プロジェクトとして公開されているポリシーテンプレート*1をダウンロードして、ドメインコントローラの C:\Windows\SYSVOL\domain\Policies\PolicyDefinitions 配下に各ファイルを配置すると、グループポリシーの管理画面上で「コンピューターの構成」および「ユーザーの構成」の「管理用テンプレート」配下にFirefox向けの設定項目が表示されるようになります。 実際にポリシーテンプレートを読み込んだ様子

一部のポリシー設定は名称が「Disable(無効化する)」「や「Block(遮断する)」となっており、「有効」「無効」の意味が直感的な意味と反転しています。このようなポリシー設定では「有効」は「対象の機能を無効化する・遮断する」、「無効」は「無効化しない・遮断しない(=対象の機能を有効化する)」という意味になりますので、誤解したまま運用しないように注意して下さい。

Firefoxの設計上は、同じポリシーがユーザー向けとコンピューター向けの両方に設定されていた場合、ユーザーに設定されたポリシーよりもコンピューターに設定されたポリシーの方が優先的に反映されます。例えば「Block About Config(about:configへのアクセスを遮断する)」というポリシーについて、ユーザー向けのポリシーで「有効(about:configを遮断)」と設定していても、コンピューターのポリシーで「無効(about:configを遮断しない)」と設定していると、このポリシーは「無効」と扱われ、about:configは各ユーザーの環境で使用可能になります。前項の点と併せて、誤解の無いようにくれぐれも注意して下さい。

なお、2018年3月23日時点で公開されているのはWindows Server 2008以降で使用できるadmx形式のファイルのみです*2。Windows 2003 ServerをドメインコントローラとしてActive Directoryを運用している場合には、上記のポリシーテンプレートを参考にadm形式のポリシーテンプレートを作成するか、もしくは前の記事で解説しているJSONファイル形式でのポリシー定義を各クライアントに配布する方法を取る必要があります。

*1 現時点では言語リソースは英語用のみが提供されています。翻訳してプルリクエストを出してみるのも良いかもしれません。ぜひ挑戦してみて下さい。

*2 Windows Server 2003ドメインでもこのポリシーテンプレートは有効ですが、ドメインコントローラはWindows Server 2008以降のバージョンである必要があります。

つづき: 2018-05-12
タグ: Mozilla
2018-03-23

Firefox ESR60以降でのPolicy Engineによるポリシー定義の手順

Firefox ESR60以降のバージョンでは、Policy Engineと呼ばれる新しいポリシー設定の仕組みが導入されます。近いうちに公式なドキュメントが用意されるものと予想されますが、Firefoxを法人で利用中の場合、一日も早く実際の動作を検証したいというニーズもある事でしょう。この記事では、現時点で実装されているPolicy Engineの具体的な使用手順を解説します。

準備

Policy EngineはNightly 60で既に使用可能な状態になっています。Firefox開発版のダウンロードページから最新の開発版であるNightlyをダウンロードし、インストールしておきます。

次に、Nightlyの起動用ショートカットを編集します。Firefoxは通常版と開発版(Nightly)でプロファイルの内容に互換性が無い場合があり、普段使いのFirefoxのプロファイルでNightlyを起動してしまうと、不可逆的な移行処理が行われてしまって、以後通常版のFirefoxでそのプロファイルを使用できなくなる可能性があります。Firefoxのショートカットのプロパティを開き、「リンク先」欄の末尾にC:\Program Files\Nightly\firefox.exe -profile %temp%\NightlyProfile -no-remoteのように起動プロファイルを明示しておく事で、普段使いのプロファイルとは別の専用プロファイルでNightlyを起動できるようになります。

Nightlyの準備が終わったら、Policy Engineの設定の準備です。

Policy Engine用のポリシー設定は、Windowsの場合はレジストリ経由(グループポリシーオブジェクトでの設定)とJSONファイル経由での設定の2通りの方法があります。 簡単のため、ここではJSONファイルを使う方法のみ解説します。

ポリシー設定のためのJSONファイルは、Firefoxのインストール先フォルダ配下にdistributionという名前でフォルダを作成し、さらにその中にpolicies.jsonという名前で以下の内容のテキストファイル(JSON形式)を設置します。

{
  "policies": {
  }
}

以上で準備は完了です。

ポリシー設定の記述の仕方

JSONファイルでのポリシー設定はpolicies.jsonpoliciesのプロパティとして記述します。現時点で使用できるポリシー設定にどのような物かがあるかはpolicies.jsonのスキーマ定義に列挙されています。

例えば、about:configで設定を変更される事とFirefoxの自動更新のそれぞれを禁止したい場合、policies.jsonは以下のように記述します。

{
  "policies": {
    "BlockAboutConfig": true,
    "DisableAppUpdate": true
  }
}

2018年3月9日現在、以下のポリシー設定が存在します。これらは仕様が変更または削除される可能性がある事、あるいは新しいポリシー設定が今後追加される可能性がある事に注意して下さい。

  • "BlockAboutAddons": trueabout:addons(アドオンマネージャ)の使用を禁止する。間接的に、アドオンのインストールを禁止する効果がある。
  • "BlockAboutConfig": trueabout:configの使用を禁止する。同時に、副作用として"DisableDeveloperTools": trueの効果も反映される。
  • "BlockAboutProfiles": trueabout:profilesの使用を禁止する。
  • "BlockAboutSupport": trueabout:supportの使用を禁止する。
  • "BlockSetDesktopBackground": true :画像をコンテキストメニューからデスクトップの壁紙に設定する機能の使用を禁止する。
  • "Bookmarks": [{"Title": "...", "URL": "...", "Favicon": "...", "Placement": "toolbar", "Folder": true/false }, ...] :ブックマークツールバーに既定のブックマーク項目を追加する。
  • "Bookmarks": [{"Title": "...", "URL": "...", "Favicon": "...", "Placement": "menu", "Folder": true/false }, ...] :ブックマークメニューに既定のブックマーク項目を追加する。
  • "Cookies": { "Allow": ["http://example.com", "https://example.org:8080"] } :指定のWebサイト(オリジンで指定)でCookie、IndexedDB、Web Storage、およびService Worker用Cacheを保存する(任意に無効化はできない)。
  • "Cookies": { "Block": ["http://example.com", "https://example.org:8080"] } :指定のWebサイト(オリジンで指定)でCookie、IndexedDB、Web Storage、およびService Worker用Cacheを保存しない(任意に有効化はできない)。また、これらのホストに保存済みのCookieがあった場合、それらは削除される。
  • "CreateMasterPassword": false : マスターパスワードを設定する事を禁止する。
  • "DisableAppUpdate": true :Firefoxの自動更新を停止する。
  • "DisableDeveloperTools": true :開発ツールの使用を禁止する。
  • "DisableFirefoxAccounts": true :Firefoxアカウントの使用を禁止する(ひいては、Firefox Syncの使用も禁止される)。
  • "DisableFirefoxScreenshots": true :Firefox Screenshotsの使用を禁止する。
  • "DisableFirefoxStudies": trueFirefoxの新機能のテストへの参加を禁止する。
  • "DisableFormHistory": true :フォームの入力履歴の保存とオートコンプリートを禁止する。
  • "DisablePocket": true :Pocketの使用を禁止する。
  • "DisablePrivateBrowsing": true :プライベートブラウジング機能の使用を禁止する。
  • "DisableSysAddonUpdate": true :システムアドオンの更新を禁止する。
  • "DisplayBookmarksToolbar": true :初期状態でブックマークツールバーを表示する。(ただしこの設定は強制でなく、ユーザーが任意に非表示にする事もでき、非表示にした場合は次回以降の起動時も非表示のままとなる。)
  • "DisplayMenuBar": true :初期状態でメニューバーを表示する。(ただしこの設定は強制でなく、ユーザーが任意に非表示にする事もでき、非表示にした場合は次回以降の起動時も非表示のままとなる。)
  • "DontCheckDefaultBrowser": true :起動時に既定のブラウザにするかどうかを確認しない。
  • "FlashPlugin": { "Allow": ["http://example.com", "https://example.org:8080"] } :指定のWebサイト(オリジンで指定)でAdobe FlashをClick to Play無しで自動実行する(任意に無効化はできない)。
  • "FlashPlugin": { "Block": ["http://example.com", "https://example.org:8080"] } :指定のWebサイト(オリジンで指定)でAdobe Flashの実行を禁止する(任意に有効化はできない)。
  • "Homepage": { "URL": "http://example.com", "Locked": true/false, "Additional": ["https://example.org:8080", ...] } :既定のホームページを設定する。"Locked"trueの場合はホームページを固定する。また、"Additional"を指定した場合は2番目以降のホームページとして設定する。
  • "InstallAddons": { "Allow": ["https://example.com", "https://example.org:8080"] } :指定のWebサイト(オリジンで指定)でアドオンのインストール時に警告しない(httpsのみ指定可能)。
  • "Popups": { "Allow": ["http://example.com", "https://example.org:8080"] } :指定のWebサイト(オリジンで指定)でwindow.open()によるポップアップを常に自動的に開く(任意に無効化はできない)。
  • "RememberPasswords": true/false :パスワードマネージャの使用または使用禁止を強制する。

ポリシー設定はFirefoxの起動時に読み込まれます。Firefoxの動作中に変更したポリシー設定は、次回のFirefox起動時から反映されます。

まとめ

以上、Firefox ESR60から使用可能になるPolicy Engineによるポリシー設定の手順を解説しました。

従来Firefoxでは、AutoConfigの一般的な設定でできないカスタマイズが必要な場合、アドオンやAutoConfig内に埋め込まれたスクリプトによってそれらを強引に実施するという方法を取る事ができました。ESR60以降のバージョンではXULアドオンが廃止され、またAutoConfigのスクリプト内での特権が必要なコードの実行が禁止される見込みであることから、それらの方法は取れなくなります。Policy Engineで実現可能な部分はPolicy Engineで設定するように、今のうちに備えておくようにしましょう。

タグ: Mozilla
2018-02-19

ある日突然Thunderbirdのアドオンが動作しなくなった、という時の原因の切り分け方

FirefoxやThunderbirdのアドオンを使用していて、ある日急に動かなくなった、という話はよくあります。 そのような場合にどうやって原因を特定し解決するのか、Thunderbird用のアドオンであるTheme Font & Size Changer for TBでの場合を例に解説してみます。 トラブルシューティングの一事例として参考にしていただければ幸いです。

アドオンの性質を見極める

まず何をおいても最初に確認するべきは、そのアドオンがどのような性質を持ちどのような機能を実現するアドオンなのかという点です。

今回問題が起こった「Theme Font & Size Changer for TB」はThunderbirdのGUIの文字サイズや配色を変更するアドオンです。 Thunderbirdではメールの内容を解釈したり表示したりする部分と全体的なUIを制御する部分とはほとんど関係がないため、例えば「受信したメールの内容」は問題と無関係である可能性が高い、といった推測ができます。 また、特定のWebサービスと連携するようなアドオンでもないため、Webサービス側からの接続遮断や、Webサービスのメンテナンスによる機能停止といった物もこの問題には関係していないと考えられます。

このように、原因究明を迅速に行うためには、可能性が低い事柄を調査の対象から一旦除外するのが有効です。

問題が起こるようになる直前に自分で行った操作の影響を疑う

次に確認するべきなのは、可能性として除外しきれなかった範囲における、問題が発生し始めた時期に行った操作との関連性です。 例えば以下のような事柄です。

  • Thunderbirdの設定を変更した
  • アドオンを新しくインストールした
  • アドオンをアンインストールした

変化が可逆的な物である場合、まずそれを元に戻してみるのが基本です。 今回の事例では問題の発生前後で特にこれといった操作を行っておらず(休暇明けに出勤したら突然アドオンが動作しなくなっていた、のような状況を想像して下さい)、ユーザーが行った操作は原因ではない可能性が高いと思われました。

Thunderbird自体の更新の影響を疑う

Thunderbirdは自動更新機能を持っており、ユーザーが気がつかないうちに新しいバージョンがダウンロードされている事があります。 アドオンが新しいバージョンのThunderbirdに対応していない場合、Thunderbirdの自動更新に伴ってアドオンが無効化されたり、アドオンが動作しなくなったりする事があります。

今回の事例では、問題の発生前後でThunderbirdの新しいリリースは行われていませんでした。 ですので、この可能性は除外しました。

アドオンの更新の影響を疑う

Thunderbird本体と同様に、自動更新によってアドオンが新しいバージョンに入れ替わった事で問題が起こる事もあります。 一般的にはあまりありませんが、「更新前は正常に動いていた機能が更新後には動かなくなる」という種類の問題(後退バグ、あるいはリグレッション)が混入する場合があるからです。

また、問題が起こっているアドオンそのもの以外の他のアドオンが更新された事がきっかけで問題が起こる事もあります。 アドオン同士が衝突して片方あるいは両方の動作が妨げられるという事例は枚挙にいとまがありません。

ただ、今回の事例では、問題の発生前後でアドオンの更新は特に発生していませんでした。 問題が起こっているアドオンの最終更新日は2017年11月で、だいぶ以前の事ですので、自動更新が急に行われたという事は考えにくいです。

アドオン自体の問題を疑う

問題の発生前後で特にこれといった変化が無かったとなると、後はアドオンの初期化処理を丁寧に追って、どの時点で問題が発生しているのかを突き止めるという事になります。

Thunderbird用アドオンは、現在Firefox用のアドオンで「レガシー」と位置づけられている種類のアドオンと同様の作りになっています。 今回問題となったアドオンの、問題発生時の最新版であるバージョン62.0は、その中でもBootstrapped Extensionと呼ばれる、アドオンのインストール・アンインストール時にThunderbird自体の再起動が不要な種類の物でした。

Bootstrapped Extensionの初期化処理のステップを追うと、以下の箇所のThemeFontSizeChangerBootstrapAddon.compile()falseを返しているために初期化処理が中断されているという事が分かりました。

function startup(data, reason) {
    try{
        if(!ThemeFontSizeChangerBootstrapAddon.compile()) return;
        ThemeFontSizeChangerBootstrapAddon.startup(data,reason);
    } catch(e){
        ThemeFontSizeChangerBootstrapAddon.lg(e,1);
    }
}

さらにこのメソッドの内容を調査すると、以下の通り実装されている事が分かりました。

...
const build = 1507391194;
...
    compile:function(){
        if((build+'').length != 10) Services.prompt.alert(null, 'CompileAddon', 'An error occured');
        if(new Date().getTime() > ((build+'').length == 10 ? build*1000 : build)+(3*30*24*60*60*1000)) return false;
        return true;
    }

new Date().getTime()は実行時のUNIX時刻をミリ秒単位で取得する物です。1507391194というのはいわゆるUNIX時間(協定世界時1970年1月1日0時0分0秒からの経過秒数による日時の表現)で2017年10月7日15時46分34秒を指しており、この箇所全体では「現在日時がその3ヶ月後(2018年1月5日15時46分34秒)を超えているとfalseが返される」という意味になっています。 つまり、それまでは全く正常に動作していたこのアドオンは、世界中で今年の1月5日以降急に動作しなくなるよう設計されていたという、時限式のタイマーが組み込まれていたという事が分かりました。

以上の事から、この問題の対策として「当該箇所を無害化する改修を施す」あるいは「実行環境の時計を意図的にずらす」のいずれかを実施する必要があるという事になります。

まとめ

「Theme Font & Size Changer for TB」を例にとって、アドオンが急に動作しなくなったという場合の調査の具体的な進め方をご紹介しました。

当社のFirefox/Thunderbrid法人向けサポートでは、アドオンに関するものについても、障害の原因調査から対策のご提案、改修版のアドオンの提供や開発元へのフィードバックなどの対応を有償にて行っております。 業務で使用しているアドオンで致命的な問題が発生していてお困りの場合には、ぜひ一度当社までご相談下さい

タグ: Mozilla
2018-01-16

FirefoxのアドオンでSVG画像を「色を変えられるアイコン」として使う方法

Photonのアイコン画像を劣化の無いSVG形式で入手する

Firefox 57以降のバージョンで採用されているアイコンや配色などの視覚的デザインセットには「Photon」という名前が付いています。サイドバーやツールバーボタンのパネルなど、Firefox用のアドオンで何らかのGUIを提供する場合には、このPhotonと親和性の高い視覚的デザインにしておく事が望ましいです。

(Photon Iconsのスクリーンショット) Photonのデザイン指針に則って作成されたFirefoxの各種アイコンは、SVG形式のデータが公開されています。SVGのようなベクター画像形式は拡大縮小しても画質が劣化しないため、極端に解像度が高い環境でもレイアウトの崩れや強制的な拡大縮小によるボケなどの発生を気にせずに使えるのが魅力です。PhotonのアイコンセットはライセンスとしてMPL2.0が設定されているため、自作のアドオンにも比較的容易に組み込んで使えますので、是非活用していきたい所です。

SVGアイコンの簡単な使い方

アドオンでSVGアイコンを使う時は、背景画像として使う方法が一番簡単です。ただし単純にある要素の背景画像に設定するのではなく、::beforeまたは::after疑似要素の背景画像として設定する方が、親要素の背景画像や枠線などと組み合わせられて何かと都合が良いのでお薦めです。

例えば何かのGUI要素に「閉じる」ボタンのようなUIを付けたい場合、そのGUI要素にあたる要素が<span href="#" class="closebox"></span>として定義されているのであれば、タブのクローズボックス等で使われているアイコン画像のclose-16.svgを以下の要領でアイコン画像として表示できます。 (::after疑似要素として表示されたSVGアイコン画像の例)

/* 対象の要素そのものではなく、::beforeや::after疑似要素にスタイル指定を行うことで、
   アイコン画像の<img>要素を挿入したように扱うことができる。 */
.closebox::after {
  /* 疑似要素を有効化するために、空文字を内容として指定する。
     (contentが無指定だと::before/::after疑似要素は表示されない。) */
  content: "";
  /* <img>と同様に、幅と高さを持つボックス状のインライン要素として取り扱う。 */
  display: inline-block;
  /* 表示したいアイコン画像の大きさを、この疑似要素自体の大きさとして設定する。 */
  min-height: 24px;
  min-width: 24px;
  /* SVG画像をボックスの大きさぴったりに拡大縮小して背景画像として表示する。背景色は透明にする。 */
  background: transparent url("./close-16.svg") no-repeat center / 100%;
}

SVGアイコンの色を変えるには?

Photonのアイコン画像はいずれも黒または白一色のべた塗りで、意味はシルエット(形)で表すようにデザインされています。これには、カラフルなアイコンだとテーマの色に合わなくなる事があるのに対し、シルエットのみのアイコンであればテーマに合わせた色に変えて使いやすいからという理由があります。

実は、背景画像として表示されるSVG画像の色は、FirefoxにおいてはCSSでの指定だけで変える事ができます。SVG画像の中で<path fill="context-fill">のようにfill="context-fill"が設定されている閉領域の塗り潰し色は、以下のようにするとCSSの側での指定を反映させられます。 (同じSVG画像を使用して、塗り潰しの色をCSSの指定で変えた状態)

.closebox::after {
  content: "";
  display: inline-block;
  min-height: 24px;
  min-width: 24px;
  background: transparent url("./close-16.svg") no-repeat center / 100%;

  /* 塗りの色を指定 */
  fill: #EFEFEE;
  /* CSSのfillプロパティの値をSVG画像のcontext-fillに反映するための指定 */
  -moz-context-properties: fill;
}

ここではカラーコードを直接指定していますが、以下のようにカスタムプロパティ(CSS変数)を使えば色の指定だけを簡単に差し替えられます。

:root {
  /* 冒頭、最上位の要素で色だけを定義 */
  --background-base-color: #EFEFFF;
  --foreground-base-color: #0D0D0C
}

...

.closebox::after {
  ...
  /* 後の箇所では定義済みの色を名前で参照する */
  fill: var(--foreground-base-color);
  ...
}

これをうまく使えば、ツリー型タブのように複数テーマを切り替えたりthemes.onUpdatedを監視して他のアドオンが設定したテーマの色を自動的に反映したりといった事も容易に実現できます。

ただし、ここで1つ残念なお知らせがあります。実は、上記の指定はFirefoxの既定の状態では機能しないのです(Firefox 57現在)。

上記のような指定はFirefox自体のGUIの外観を定義するのにも使われているのですが、ここでfillと共に使われている-moz-context-propertiesが曲者です。このプロパティは今のところFirefoxの独自拡張プロパティで、about:configproperties.content.enabledtrueに変更しない限り、アドオンが提供するサイドバーやツールバーのポップアップなどの中では使えないようになっているため、結局はSVGのアイコン画像は黒一色で表示されるという結果になってしまうのです。Bug 1388193またはBug 1421329が解消されるまでは、この方法は一般的なユーザーの環境では使えないという事になります。

maskを使った「CSSの指定だけでSVGアイコンの色を変える」代替手法

でも諦めるのはまだ早いです。Photonのアイコンセットのようにシルエットだけで構成されたSVG画像であれば、mask関連の機能で上記の例と同等の事ができます。具体的には以下の要領です。

.closebox::after {
  content: "";
  display: inline-block;
  min-height: 24px;
  min-width: 24px;
  /* SVG画像は背景画像としては使わない。 */
  /* background: transparent url("./close-16.svg") no-repeat center / 100%; */

  /* まず、アイコンの色として使いたい色で背景を塗り潰す */
  background: #EFEFEE;
  /* 次に、SVG画像をボックスの大きさぴったりに拡大縮小してマスク画像として反映する */
  mask: url("./close-16.svg") no-repeat center / 100%;
}

疑似要素自体は指定の背景色の矩形として描画されますが、その際マスク画像の形に切り抜かれるため、結果として「単色で、SVG画像のシルエットの形をしたアイコン」のように表示されるという仕組みです。

この代替手法には元の手法よりもCPU負荷が高くなるというデメリットがあります。特に:hover等の疑似クラスやアニメーション効果と組み合わせる時には、CPU負荷が一時的に100%に張り付くようになる場合もあり得ます。モバイルPCの電池の持ちが悪くなるなどの副作用が生じる事になりますので、使用は注意深く行ってください。

Bug 1388193またはBug 1421329のどちらかが解消された後は、この代替手法を速やかに削除できるように、最上位の要素のクラスなどを見て反映するスタイル指定を切り替えるのがお薦めです。以下はその指定例です。

.closebox::after {
  content: "";
  display: inline-block;
  min-height: 16px;
  min-width: 16px;
  /* 将来的に反映したい指定 */
  background: url("./close-16.svg") no-repeat center / 100%;
  fill: #EFEFFF;
  -moz-context-properties: fill;
}

:root.simulate-svg-context-fill .closebox::after {
  /* 後方互換性のための代替手法の指定 */
  background: #EFEFFF;
  mask: url("./close-16.svg") no-repeat center / 100%;
}

まとめ

FirefoxのアドオンでSVG画像をアイコンとして使う場合の小技をご紹介しました。

GUIを持つアドオンを作る場合、ユーザーを迷わせないで済むように、アイコン画像はなるべくFirefox本体の物とデザインを揃えておいた方が良いです。Photonのアイコンセットを使い、皆さんも洗練されたデザインのGUIを実装しましょう。

タグ: Mozilla
2017-12-26

Firefoxのアドオンで、一般的な方法では分からないタブの状態を判別する

Firefoxのタブを参照するアドオンは、browser.tabs.get()browser.tabs.query()などのAPIを使って各タブの状態を取得します。この時、Firefoxのタブの状態を表すオブジェクトはtabs.Tabという形式のオブジェクトで返されます。

tabs.Tabにはタブの状態を表すプロパティが多数存在していますが、ここに表れないタブの状態という物もあります。「未読」「複製された」「復元された」といった状態はその代表例です。これらはWebExtensions APIの通常の使い方では分からないタブの状態なのですが、若干の工夫で判別することができます。

タブが未読かどうかを判別する方法

タブの未読状態は、バックグラウンドのタブの中でページが再読み込みされたりページのタイトルが変化したりしたらそのタブは「未読」となり、タブがフォーカスされると「既読」となります。これは、tabs.onUpdatedtitleの変化を監視しつつ、tabs.onActivatedでタブの未読状態をキャンセルする、という方法で把握できます。以下はその実装例です。

// バックグラウンドページで実行しておく(tabsの権限が必要)
var gTabIsUnread = new Map();

browser.tabs.onUpdated.addListener((aTabId, aChangeInfo, aTab) => {
  // アクティブでないタブのタイトルが変化したら未読にする
  if ('title' in aChangeInfo && !aTab.active)
    gTabIsUnread.set(aTabId, true);
});

browser.tabs.onActivated.addListener(aActiveInfo => {
  // タブがアクティブになったら既読にする
  gTabIsUnread.delete(aActiveInfo.tabId);
});

browser.tabs.onRemoved.addListener((aTabId, aRemoveInfo) => {
  // タブが閉じられた後は未読状態を保持しない
  gTabIsUnread.delete(aTabId);
});

// 上記の処理が動作していれば、以下のようにしてタブの未読状態を取得できる。
// var unread = gTabIsUnread.has(id);

上記の例ではMapで状態を保持していますが、Firefox 57以降で使用可能なbrowser.sessions.setTabValue()browser.sessions.getTabValue()を使えば、名前空間をまたいで状態を共有する事もできます。以下はその例です。

// バックグラウンドページで実行しておく(tabs, sessionsの権限が必要)

browser.tabs.onUpdated.addListener((aTabId, aChangeInfo, aTab) => {
  // アクティブでないタブのタイトルが変化したら未読にする
  if ('title' in aChangeInfo && !aTab.active)
    browser.sessions.setTabValue(aTabId, 'unread', true);
});

browser.tabs.onActivated.addListener(aActiveInfo => {
  // タブがアクティブになったら既読にする
  browser.sessions.removeTabValue(aActiveInfo.tabId, 'unread');
});

// 上記の処理が動作していれば、以下のようにしてタブの未読状態を取得できる。
// var unread = await browser.sessions.getTabValue(id, 'unread');

タブが複製された物か、復元された物かを判別する

WebExtensionsではタブが開かれた事をtabs.onCreatedで捕捉できますが、そのタブが既存のタブを複製した物なのか、閉じられたタブが復元された物なのか、それとも単純に新しく開かれたタブなのか、という情報はtabs.Tabからは分かりません。しかし、タブのセッション情報を使えばこれらの3つの状態を判別できます。

判別の方法

複製や復元されたタブは、元のタブにbrowser.sessions.setTabValue()で設定された情報を引き継ぎます。この性質を使うと、以下の理屈でタブの種類を判別できます。

  1. browser.sessions.getTabValue()でIDを取得してみて、取得に失敗したら(IDが保存されていなければ)そのタブは新しく開かれたタブである。
  2. IDの取得に成功し、そのIDを持つタブが既に他に存在しているのであれば、そのタブは複製されたタブである。
  3. IDの取得に成功し、そのIDを持つタブが他に存在していないのであれば、そのタブは一旦閉じられた後に復元されたタブである。

(この判別方法にはFirefox 57以降で実装されたsessions APIbrowser.sessions.setTabValue()browser.sessions.getTabValue()という2つのメソッドが必要となります。そのため、これらが実装される前のバージョンであるFirefox ESR52などではこの方法は使えません。また、これらのメソッドは今のところFirefoxでのみ実装されているため、GoogleChromeやOperaなどでもこの方法を使えないという事になります。)

以上の判別処理を実装すると、以下のようになります。

// バックグラウンドページで実行しておく(tabs, sessionsの権限が必要)

// IDからタブを引くためのMap
var gTabByPrivateId = new Map();
// 判別結果を保持するためのMap
var gTabType = new Map();

// 一意なIDを生成する(ここでは単に現在時刻とランダムな数字の組み合わせとした)
function createNewId() {
  return `${Date.now()}-${parseInt(Math.random() * Math.pow(2, 16))}`;
}

// タブの種類を判別する
async function determineTabType(aTabId) {
  // セッション情報に保存した独自のIDを取得する
  var id = await browser.sessions.getTabValue(aTabId, 'id');
  if (!id) {
    // 独自のIDが保存されていなければ、そのタブは一般的な新しいタブであると分かるので
    // 新たにIDを振り出す
    id = createNewId();
    // 振り出したIDをセッション情報に保存する
    await browser.sessions.setTabValue(aTabId, 'id', id);
    // IDでタブを引けるようにする
    gTabByPrivateId.set(id, aTabId);
    return { type: 'new', id };
  }

  // 独自のIDが保存されていれば、そのタブは複製されたタブか復元されたタブということになる

  // そのIDをもつタブが存在するかどうかを調べる
  let existingTabId = gTabByPrivateId.get(id);

  // タブが存在しない場合、このタブは「閉じたタブを開き直す」またはセッションの復元で
  // 開き直されたタブであると分かる
  if (!existingTabId) {
    gTabByPrivateId.set(id, aTabId);
    return { type: 'restored', id };
  }

  // タブが存在していて、それが与えられたタブと同一である場合、
  // この判別用メソッドが2回以上呼ばれたということになる
  if (existingTabId == aTabId)
    throw new Error('cannot detect type of already detected tab!');

  // タブが存在しているが、与えられたタブではない場合、このタブは
  // そのタブを複製したタブであると分かるので、新しいIDを振り出す
  id = createNewId();
  await browser.sessions.setTabValue(aTabId, 'id', id);
  gTabByPrivateId.set(id, aTabId);
  return { type: 'duplicated', id, originalId: existingTabId };
}

browser.tabs.onCreated.addListener(async (aTab) => {
  // 新しく開かれたタブに対する任意の処理
  // ...

  // タブの種類の判別を開始する
  var promisedType = determineTabType(aTab.id);
  // 判別結果を他の箇所からも参照できるようにしておく
  gTabType.set(aTab.id, promisedType);
  var type = await promisedType;

  // 上記判別結果を使った、新しく開かれたタブに対する任意の処理
  // ...
});

browser.tabs.onRemoved.addListener(async (aTabId, aRemoveInfo) => {
  // 削除されたタブに対する任意の処理
  // ...

  // それぞれのMapから閉じられたタブの情報を削除する
  var type = await gTabType.get(aTabId);
  gTabByPrivateId.delete(type.id);
  gTabType.delete(aTabId);
});

// 既に開かれているタブについての初期化
browser.tabs.query({}).then(aTabs => {
  for (let tab of aTabs) {
    gTabType.set(tab, determineTabType(tab.id));
  }
});
他のイベントも監視する場合の注意点

上記のようにしてtabs.onCreatedでタブの種類を判別してからその他の初期化処理を行う場合、タブの種類の判別は非同期に行われるため、tabs.onCreatedのリスナーが処理を終える前に他のイベントのリスナーが呼ばれる事もある、という点に注意が必要です。tabs.onUpdatedtabs.onActivatedのリスナーが、tabs.onCreatedで何らかの初期化が行われている事を前提として実装されている場合、上記の判別処理やその他の非同期処理が原因で初期化が終わっていないタブが他のリスナーに処理されてしまうと、予想もしないトラブルが起こる可能性があります。

そのようなトラブルを防ぐためには、以下のようにしてタブの初期化処理の完了を待ってからその他のイベントを処理するようにすると良いでしょう。

var gInitializedTabs = new Map();

browser.tabs.onCreated.addListener(async (aTab) => {
  var resolveInitialized;
  gInitializedTabs.set(aTab.id, new Promise((aResolve, aReject) => {
    resolveInitialized = aResolve;
  });

  // 任意の初期化処理
  // ...

  resolveInitialized();
});

// 別のウィンドウから移動されたタブに対してはtabs.onCreatedは発生しないため、
// tabs.onAttachedも監視する必要がある
browser.tabs.onAttached.addListener(async (aTabId, aAttachInfo) => {
  var resolveInitialized;
  gInitializedTabs.set(aTabId, new Promise((aResolve, aReject) => {
    resolveInitialized = aResolve;
  });

  // 任意の初期化処理
  // ...

  resolveInitialized();
});


browser.tabs.onUpdated.addListener(async (aTabId, aChangeInfo, aTab) => {
  await gInitializedTabs.get(aTabId);

  // 以降、タブの状態の更新に対する任意の処理
  // ...
});

browser.tabs.onActivated.addListener(async (aActiveInfo) => {
  await gInitializedTabs.get(aActiveInfo.tabId);

  // 以降、タブのフォーカス移動に対する任意の処理
  // ...
});

// メッセージの処理
browser.runtime.onMessage.addListener((aMessage, aSender) => {
  // この例では、必ずメッセージの`tabId`というプロパティでタブのIDが渡されてくるものと仮定する
  if (aMessage.tabId) {
    let initialized = gInitializedTabs.get(aMessage.tabId);
    if (!initialized)
      initialized = Promise.resolve();
    // async-awaitではなく、Promiseのメソッドで初期化完了を待つ
    // (関数全体をasyncにしてしまうと、このリスナが返した値が必ず
    // メッセージの送出元に返されるようになってしまうため)
    initialized.then(() => {
      // 初期化済みのタブを参照しての何らかの処理
      // ...
    });
  }
  // その他の処理
  // ...
});

// 他のアドオンからのメッセージの処理
browser.runtime.onExternalMessage.addListener((aMessage, aSender) => {
  // ここでもbrowser.runtime.onMessageのリスナーと同じ事を行う
  // ...
});
tabs.onUpdatedを監視する場合の、Bug 1398272への対策

ここまでの実装例でtabs.onUpdatedを監視する例を示してきましたが、現時点での最新リリース版であるFirefox 57には、tabs.onUpdatedを監視しているとウィンドウをまたいでタブを移動した後にタブのIDの一貫性が損なわれる(本来であればウィンドウをまたいで移動した後もタブのIDは変わらない事が期待されるのに対し、このBugの影響により、ウィンドウをまたいで移動したタブに意図せず新しいIDが振り出されてしまう)という問題があります。

この問題を回避するには、IDの振り出し状況を監視して対応表を持つ必要があります。単純ではありますが、エッジケースの対応なども考慮に入れると煩雑ですので、この問題のWorkaroundとして必要な一通りの処理をまとめたwebextensions-lib-tab-id-fixerというライブラリを作成・公開しました。tabs.onUpdatedを監視する必要があるアドオンを実装する場合には試してみて下さい。

まとめ

WebExtensions APIで一般的には提供されていないタブの状態の情報について、既存APIの組み合わせで間接的に状態を判別する方法をご紹介しました。

現時点でFirefoxにのみ実装されているsessions APIの機能には、このような意外な応用方法があります。皆さんも、今あるAPIを違った角度から眺めてみると、APIが無いからと諦めていた事について実現の余地が見つかるかもしれませんので、色々試してみる事をお薦めします

タグ: Mozilla
2017-12-22

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|
タグ: