本番環境でやらかさないために独自ブラウザ拡張を作ってみた

webアプリを開発していると、開発環境だと思って画面を操作していたら実は本番環境だった！
なんてことがありませんか？ 私はあります。。。

幸い大事に至ったことはありませんが、何かやらかしてしまう前に対策をしようと、本番環境のときだけアラートを表示するブラウザ拡張とかをさくっと作れたりしないかなーと思って調べてみたところ、「`特定ページ` を開いたときに `js, css` を適用する」程度の独自ブラウザ拡張（ストア公開なし）だったら簡単に作ることができたのでご紹介したいと思います。

※ 対象環境は **macOS** の **Google Chrome** と **Safari** になります。

# 完成品サンプル

具体的な作り方の前に、出来上がったもののサンプルを載せておきます。
今回は「`特定ページ（qiita.com）` を開いたときに `js, css （シングルクリックすると四隅を移動し、ダブルクリックすると消える鬱陶しい警告を表示する）` を適用する」ブラウザ拡張を作ってみました。
特定ページ以外（qiita.com 以外）を開いたときは警告は表示されませんが、 特定ページ（qiita.com）を開いたときは js, css が適用され警告が表示されるようになっています。

※ サンプルとして載せるために `特定ページ` を `qiita.com` としていますが、実際はここを本番環境のドメインにして使用しています。

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320080/d1fd582f-a203-a567-a131-1863eceea691.gif)

# 作り方

それでは具体的な作り方をご紹介していきます。

※ 今回はブラウザ拡張の名前を「Heads-up」にしたのでファイル名や設定値等でこの単語が登場しますが、任意の名称を指定している箇所として読み替えてください。

## js, css の作成

まず最初に、特定ページを開いたときに適用したい `js, css` を作成しておきます。
ここは本題ではないので詳細は割愛してソースだけ載せておきます。


```js:heads-up.js
window.onload = function () {
    var div = document.createElement('div');
    div.id = 'heads-up-wrapper';
    let positions = ['top_left', 'top_right', 'bottom_right', 'bottom_left'];
    div.className = positions[0];
    var clickFlg = false;
    div.onclick = function(e) {
        // ダブルクリック
        if (clickFlg) {
            // div を削除
            div.parentNode.removeChild(div);

            clickFlg = false;
            return;
        }

        // シングルクリック
        clickFlg = true;
        setTimeout(function () {
            if (clickFlg) {
                // div を時計回りに移動
                var i = positions.indexOf(div.className);
                div.className = positions[(i + 1) % 4];
            }

            clickFlg = false;
        }, 200);
    };

    var span = document.createElement('span');
    span.id = 'heads-up-text';
    span.innerText = document.domain;

    div.appendChild(span);
    document.body.appendChild(div);
};
```

```css:heads-up.css
#heads-up-wrapper {
    position: fixed;
    display: inline-block;
    margin: 0 auto;
    padding: 3px 6px;
    height: auto;
    width: auto;
    background-color: yellow;
    z-index: 99999;
    font-size: 16px;
    font-family: sans-serif;
    border: 2px outset red;
    box-shadow: 0px 0px 1px #5b5b5b;
    border-radius: 3px;
}
#heads-up-wrapper.top_left {
    top: 0px;
    left: 0px;
}
#heads-up-wrapper.top_right {
    top: 0px;
    right: 0px;
}
#heads-up-wrapper.bottom_left {
    bottom: 0px;
    left: 0px;
}
#heads-up-wrapper.bottom_right {
    bottom: 0px;
    right: 0px;
}
#heads-up-text {
    color: red;
    font-weight: bold;
    animation: flash 1.0s ease-in-out infinite alternate;
}
@keyframes flash {
    0% {opacity:0;}
    100% {opacity:1;}
}

```

js, css を作成したら、これをブラウザ拡張に落とし込んでいきます。

## Google Chrome

### (1) manifest.json を作成

Chrome 拡張の設定全般を記述する設定ファイルで、必須ファイルです。
今回作成したソースは以下になります。

```js:manifest.json
{
    "name": "Heads-up",
    "version": "1.0.0.0",
    "manifest_version": 2,
    "description": "Heads-up!",
    "content_scripts": [
        {
            "matches": [ "https://qiita.com/*" ],
            "js": ["heads-up.js"],
            "css": ["heads-up.css"]
        }
    ]
}
```

各定義についてざっくり解説します。
※ 今回定義したのは必要最低限の項目のみなので、その他詳細については [公式ドキュメント](https://developer.chrome.com/extensions) をご参照ください。

- name: 拡張機能の名前
- version: 拡張機能のバージョン
- manifest_version: マニフェストファイル自身のバージョン (現在有効なバージョンは 2 のみ)
- content_scripts: [コンテントスクリプト](https://developer.chrome.com/extensions/content_scripts) 定義
	- matches: 特定ページとして指定したい URL を Match Patterns の形式で定義
	- js: 特定ページで動作させるスクリプトを定義
	- css: 特定ページに適用するスタイルシートを定義

### (2) 拡張機能フォルダを作成

任意の場所にフォルダを作成し、manifest.json, js, css ファイルをそのフォルダに格納します。

```
Heads-up
├── heads-up.css
├── heads-up.js
└── manifest.json
```

### (3) ブラウザに適用する

Chrome のメニューから `[ウィンドウ] > [拡張機能]` を選択して拡張機能画面を表示し、画面左上の `[パッケージ化されていない拡張機能を読み込む]` を押下して `(2) で作成したフォルダ` を選択すると、拡張機能として Chrome に適用されます。

### ※ ブラウザから削除したい場合

通常の拡張機能と同様、拡張機能画面で対象の拡張機能の `[削除]` を押下すれば削除されます。

## Safari （機能拡張ビルダー で作成する場合）

Safari 12 (macOS Mojave) では **機能拡張ビルダー** というブラウザの独自機能拡張作成ツールが存在したため、そちらを利用した作成方法をご紹介します。

### 事前準備

事前準備として以下を実施しておく必要があります。

- Safari のメニューから `[Safari] > [環境設定...]` を選択して設定画面を表示し、 `[詳細]` 画面の `メニューバーに"開発"メニューを表示` をチェック
- Safari のメニューから `[開発] > [未署名の機能拡張を許可]` をチェック（ ※ Safari を終了するとチェックが外れてしまうので、再起動後はチェックし直す必要があります）

### (1) 機能拡張ビルダーで .safariextz Extension を作成

Safari のメニューから `[開発] > [機能拡張ビルダーを表示]` を選択すると機能拡張ビルダーが表示されます。
画面左下の `[＋] > [新規機能拡張...]` を選択し、名前と場所を入力して `[保存]` を押下すると以下のようなディレクトリとファイルが作成されます。

```
Heads-up
└── info.plist
```

上記ディレクトリに js, css を格納し、機能拡張ビルダーで下記項目を設定します。

- Webサイトアクセス
	- アクセスレベル: `一部`
	- 許可されたドメイン: `特定ページとして指定したいドメイン`
	- セキュリティ保護されたページを表示: `チェック`
- 差し込みコンテンツ
	- 終了スクリプト: `特定ページで動作させるスクリプト`
	- スタイルシート: `特定ページに適用するスタイルシート`

機能拡張ビルダーで設定した内容は `info.plist` に反映されます。

![s12_Info.plist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320080/d30dd8c3-c4f0-c62f-74b7-60b9c86d2c48.png)

### (2) ブラウザに適用する

機能拡張ビルダー画面右上の `[実行]` を押下するとパスワード入力を求めるダイアログが表示されるので、パスワードを入力して `[OK]` を押下すると機能拡張として Safari に適用されます。

### ※ ブラウザから削除したい場合

機能拡張設定画面で対象の機能拡張を選択し `[アンインストール]` を押下すると機能拡張設定画面から削除されます。
この状態だと機能拡張ビルダー上には残ったままなので、機能拡張ビルダー上からも削除したい場合は対象の機能拡張名の右横の `[×]` （フォーカスすると表示されます）を押下すると機能拡張ビルダー上からも削除されます。

## Safari （Xcode で作成する場合）

Safari 13 (macOS Catalina) では機能拡張ビルダーが廃止されてしまいました。。

https://developer.apple.com/safari/extensions/

> Converting Legacy Safari Extensions
> 
> Legacy Safari Extensions (.safariextz files) built with Safari Extension Builder are not supported in Safari 13 on macOS Catalina, macOS Mojave, or macOS High Sierra. The Safari Extensions Gallery for legacy extensions will no longer be available in September, 2019. Users on macOS High Sierra or later can easily find extensions on the Mac App Store by choosing Safari Extensions from the Safari menu.
> 
> If you distribute legacy extensions built with Safari Extension Builder, we recommend converting them to the new Safari App Extension format, test on the latest version of Safari 13, and submit them to the Mac App Store or notarize them for distribution outside the Mac App Store.

そもそもレガシーな機能拡張（.safariextz 形式） は使えないということで、**Xcode** を使用して **Safari App Extension** を作る方法をご紹介します。

### 事前準備

[Safari （機能拡張ビルダー で作成する場合） の事前準備](https://qiita.com/items/ce87f1c312aeee9f1089#事前準備) と同様、未署名の機能拡張を許可しておく必要があります。

### (1) Xcode で Safari App Extension を作成

・ Xcode のメニューバー から `File > New > Project...` を選択
・ `macOS > Other > Safari Extension App` を選択して `Next` を押下
・ プロダクト名等の入力画面が表示されるので適宜入力して `Next` を押下（ ※ 言語は任意ですが、今回は `Swift` を指定した例になります）

ここまでで土台となるプロジェクトが作成されます。
あとはこのプロジェクトに機能拡張ビルダーで設定した内容と同じものを入れ込むだけです。 

・ `[プロジェクト名] Extension` ディレクトリ 配下に `js, css` を追加
![s13_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320080/dc85ce75-dcb2-0d5d-0471-c341d892867f.png)

・ `[プロジェクト名] Extension > info.plist` を開き下記の通り編集

![s13_info.plist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320080/8656c9d6-2184-c373-65f2-a855522d1c39.png)

以上で必須の実装は完了です。

### (2) ブラウザに適用する

Xcode 左上の `[▶︎]` (Build and then run the current scheme) を押下するとアプリケーションが起動しアプリ名とボタンのみのウィンドウが表示されます。
`[Open in Safari Extensions Preferences...]` を押下すると Safari の機能拡張設定画面が表示されるので、作成した機能拡張にチェックを入れれば機能拡張として Safari に適用されます。

※ ビルドが成功すると `app` ファイルが生成されるので、このファイルだけをコピーして配布しても使用することができます。
### ※ ブラウザから削除したい場合

Safari の機能拡張設定画面で対象の機能拡張を選択し `[アンインストール]` を押下するとアプリを削除する必要がある旨が表示されるので、 `[Finderに表示]` を押下して対象の `app` ファイルを削除すると機能拡張から削除されます。


### (おまけ)

アプリを起動して `[Open in Safari Extensions Preferences...]` を押下した際、機能拡張設定画面が表示された後もアプリが起動し続けるのが邪魔だったので `[Open in Safari Extensions Preferences...]` 押下時に自動でアプリを終了するようにしてみました。
`[プロジェクト名] > ViewController.swift` に `NSApplication.shared.terminate(self)` の処理を追記するだけでいけました。

```swift:ViewController.swift
...

class ViewController: NSViewController {

...
    
    @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) {
        SFSafariApplication.showPreferencesForExtension(withIdentifier: "com.yourcompany.Haeds-up-Extension") { error in
            if let _ = error {
                // Insert code to inform the user that something went wrong.

            }

            // ↓ ここを追記
            DispatchQueue.main.async {
                NSApplication.shared.terminate(self);
            }
            // ↑ ここを追記
        }
    }

...

}

```

# まとめ

今回は「簡単に」が目標だったので、色々と手抜き過ぎていまいちブラウザ拡張である必要性がないものになってしまいましたが、とりあえず本番環境を開発環境と誤認することはなくなったので個人的には役に立っています。

今度はツールバーやオプション設定等も作り込んで、もっと有用性のあるブラウザ拡張を作ってみたいと思いました。