読者です 読者をやめる 読者になる 読者になる

Safari拡張の作り方

Safari extension

Safari拡張をいくつか作って大体感覚は掴めたので、ざっくりと拡張の作成手順を解説してみます。
なお、Windows版で作業していますが、Macでもほとんど同じだと思います。

Safari拡張とは

最初に、Safari拡張とはなにか、について。Safari拡張はHTML/CSS/JavaScriptをベースに、ブラウザ側が用意したAPIを使ってブラウザを便利にするモジュールです。通常、JavaScriptだけではクロスドメインの問題など、実現できることに制限がありますが、その点を拡張用に用意されたAPIで補います。そのため、APIが用意されていない部分については対応できないという制限があります。しかし、開発のし易さ、ウェブとの親和性の高さからアイディア次第で便利で強力なツールとなるのがSafari拡張・Chrome拡張です。
なお、現状のAPIは暫定的なもので、ここで紹介するのもあくまで2010年6月10日時点のものです。特に正式にSafari拡張がリリースされるまでは大きな仕様変更があるかもしれないのでご注意を。
ちなみに、Chrome拡張開発者には、Chrome拡張のSafari版、の一言で十分でしょう。APIも大体似ていますし、ベースがWebKitであり、V8とJavaScriptCoreもよく似ている(似せてある)ので、開発の上では(最初は)ほとんど差を感じないと思います。
また、SafariにはGreaseKitというGreasemonkey Scriptsを動かすプラグインがありますが、Safari拡張ではGreasemonkey Scriptsと同等かそれ以上のことを実現できます。
なお、開発にはもちろんWeb Inspectorが大活躍します。もしWeb Inspectorをご存じない場合は続・先取り! Google Chrome Extensions:第6回 Firebug要らずなChromeのWeb Inspector|gihyo.jp … 技術評論社をどうぞ。
公式のドキュメントはLoading…です。Loading…からLoading…Loading…Loading…Loading…Loading…Loading…などのサンプルも公開されています。また、Chrome拡張やGreasemonkeyなどの開発者向けのドキュメントもあります。Loading…

開発手順

機能拡張を有効に

まず、設定→詳細から、「メニューバーに"開発"メニューを表示」にチェックを入れます。その開発メニューに「機能拡張を有効する」*1という選択があるのでこれをチェックします。チェックすると機能拡張ビルダーを起動することができるようになり、この機能拡張ビルダーを使ってSafari拡張を作成します。
f:id:os0x:20100610063449p:image
機能拡張ビルダーを立ち上げると最初は何も表示されていないので、左下の+アイコンをクリックしてください。すると「新規機能拡張…」「機能拡張を追加」という2つのメニューが表示されます。新規の方は名前の通り新たに拡張を作る際に、追加のほうは既存の拡張を開発モードにするときに使用します(Safari拡張でないフォルダを選択できてしまいますが、インストールできないので注意)。
f:id:os0x:20100610063450p:image
さて、「新規機能拡張…」を選択するとファイルを選択するダイアログが出てきます。ここは適当にワークスペースとする場所を決めて、適当なファイル名(その拡張の表示名になりますが、あとで変更可能です)を入力してください。

証明書を取得

ファイルを決めると機能拡張ビルダーにその拡張が表示されます。ここで、証明書がないため…というエラーがでていると思います。この状態ではインストールもパッケージングもできないので、証明書を取得する必要があります(Safari Extension 三分クッキング! - こたにきに詳しく書かれています。)。
というわけで、Safari Developer Programに登録します。Apple IDを持っていない場合、まずはApple IDを取得する必要があります。たくさんの質問に答えてなんとか登録が完了したら、Safari Extension Certificate Utilityで証明書を発行します。Macの場合、まずローカルのキーチェーンアクセス.appで証明書要求用ファイルを作成、その後、Safari Extension Certificate Utilityで証明書(safari_identity.cer)を作成・ダウンロードし、作成したsafari_identity.cerをダブルクリックしてインストールします。Windowsの場合「Windows Safari Extension Certificate Assistant」の案内に従って、テキストファイル(http://devimages.apple.com/safari/files/certreq.txt )を保存、cmdで certreq -new certreq.txt newcsr.pem を実行(certreqコマンドがない場合(Windows7だとデフォルトで入ってる?)はLoox Uと初音ミクで行こう!: Safariの証明書作成でハマった。)。newcsr.pemをアシスタントにアップロード。safari_identity.cerをダウンロードできるので、これをダブルクリックしてインストールすれば完了。割と簡単です。

機能拡張ビルダー

さて、証明書がインストールされると、先程の機能拡張ビルダーの表示が次のようになります。
f:id:os0x:20100610062940p:image
Safari Developer: (LAM47A73AC) と表示されています。このLAM47A73ACは私という開発者に当てられた識別子で、私が作成するすべての拡張はこの開発者IDを含みます。上記画像の下の方にバンドル識別子という名前の"この拡張の識別子"がありますが、このバンドル識別子とIDをあわせたものが、この拡張のドメインになります。上記の拡張の場合、 net.os0x.test2-LAM47A73AC になります。なお拡張のスキームはsafari-extension:です。例えば、AutoPatchWorkをインストールしている場合、safari-extension://net.os0x.autopatchwork-LAM47A73AC/apw_128.png にアクセスするとアイコンを表示できると思います。このようにSafari拡張はその拡張ごとにウェブサイトを持っているかのように動作します(このあたりの動作もChrome拡張とほとんど同じです)。
ビルダーで定義した内容は 拡張名.safariextension フォルダの中にInfo.plistという名前のXMLファイルとして保存されます。これはChrome拡張のmanifest.jsonに当たるファイルです。
さて、この機能拡張ビルダーを使いながら拡張を作っていきます。

“Injecting Styles”

まずは簡単なところでサイトにCSSを適用してみましょう。適当なcssファイルを用意します。名前はなんでも構いません。中身はとりあえずこんな感じにしておきます。

body{
	zoom:0.5;
}

cssファイルを作業フォルダに保存し、機能拡張ビルダーの下の方に「スタイルシート」の項目があります。新規スタイルシートをクリックすると、ボックスが現れ、保存したcssファイルが選択できます。cssファイルを選択したら、その下のホワイトリストの新規URLパターンをクリックして適用するURLを入力します。このURLパターンには*(ワイルドカード)が使えますが、ワイルドカードが使えるのはドメイン部分については//の直後だけという条件があります。このあたりもChrome拡張と同じ仕様です。なので、詳しいマッチパターンはMatch Patterns - Google Chrome(日本語訳:マッチパターン | Chrome Extensions API リファレンス)を見るとわかりやすいと思います。
f:id:os0x:20100610064227p:image
さて、この2つだけではまだCSSを適用できません。もう一つ、機能拡張ビルダーの真ん中ぐらいに「機能拡張Webサイトアクセス」という項目があります。ここでアクセスレベルを設定しないとCSSの他、JavaScriptも適用されません。アクセスレベルはまず「なし」、「一部」、「すべて」の3つから選び、一部の場合はさらにホワイトリストと同じくURLのパターンを入力します。ここで許可したサイトには、CSSJavaScriptを適用できるほか、XMLHttpRequestでクロスドメイン通信をすることも可能になります。そのため安易に「すべて」を選ばない方が良いでしょう。Chrome拡張ではすべてのサイトにアクセス可能な拡張には、ユーザーがインストールする際にその旨を警告します。Safari拡張でも同様の対応が実装されることになると思われます。
さて、アクセスレベルを設定したら、右上のインストールをクリックしてみましょう。ホワイトリストで指定したページにCSSが適用されるはずです。なお、このCSSは新たに開いたページだけでなく、インストール時に表示していたページにも適用されます。(ただ、一度適用されたスタイルはCSSを編集して再度読み込むをクリックしても反映されないことがあります。おそらくバグだと思われます。)

“Injecting Scripts”

続いて、JavaScriptを適用してみましょう。方法はCSSと同じでjsファイルを作業フォルダに保存します。JavaScriptについては、「スクリプトを開始」と「スクリプトを終了」の2つがあります。「スクリプトを開始」はページの読み込みが開始したタイミングで、document.headやdocument.bodyが存在しない状態です。唯一存在するDOMはdocumentElementだけで、document.documentElement.outerHTMLはを返す状態です。このため、DOM操作などは基本的に行えません。その代わり、早めに実行したいアクション(特定ファイルの読み込みをブロックする、読み込み自体を中止する、URLと関連付けたAPIを呼び出しておく)をいち早く実行する際に使用します。「スクリプトを終了」はDOMContentLoaded相当のタイミングで実行されるスクリプトで、ページのDOMが構築されているので、ページの内容に対してアクションを起こすことができます。
なお、このInjecting Scriptsは拡張ごとに独立したコンテキスト(もしくは名前空間)で実行されるので、ある拡張で定義した変数・関数が他の拡張だったり、サイト側などから参照できてしまうことはありません(できません)。このあたりもChrome拡張のContentScriptsと全く同じ動作です。
また、このInjecting Scriptsはグローバル変数としてsafariというオブジェクトを持ち、いくつかのAPIが定義されています。このAPIを使って後で解説するGlobal HTML PageやExtension barと連携することができます。
Injecting Scriptsは表示しているページのDOMにアクセスできますが、逆に言えばDOMの影響を受ける→悪意のあるサイトからScriptを操作される可能性があるということです。Injecting Scriptsと通常のサイト側のScriptはコンテキストが異なるので簡単には乗っ取ることはできませんが、コード次第では安全ではありません。そのため、Injecting Scriptsからは拡張のAPIの利用が大幅に制限されています(クロスドメイン通信やタブの操作などもできませんし、設定を読み取ることもできません)。ただ、Global Pageとは通信が可能なので、Global Pageを経由して各種APIを操作することになります。

“Global HTML Page”

機能拡張グローバルページでは、1つの拡張につき1つだけブラウザの起動中バックグラウンドで動き続けるページを持つことができます。Chrome拡張で言うBackground Pagesそのものです。
1つの拡張に1つだけと保証されているので、メールチェックなどのタスク、大きなデータの処理など、拡張の中心的な機能を担うことに適しています。
では、Injecting ScriptsからURLをGlobal Pageに渡し、URLに応じたデータをInjecting Scriptsに戻す簡単なサンプルコード書いてみます。
Injecting Scripts

safari.self.addEventListener('message',function(evt){
	console.log(evt.message);// Global Pageから受信
},false);
safari.self.tab.dispatchMessage('URL',location.href); // Global Pageに送信

Global Page

safari.application.addEventListener('message',function(evt){
	var data = evt.message;// Injecting Scriptsからのメッセージ
	/*何かしらの処理*/
	// レスポンスを返す
	evt.target.page.dispatchMessage('Response', custom_data);
	// もしくは
	safari.application.activeBrowserWindow.activeTab.page.dispatchMessage('Response',custom_data);
	// ならアクティブなウィンドウのアクティブなタブにメッセージが送信される
	// つまり、受信したページに返すとは限らない
},false);

また、Global Pageでは機能拡張の設定で定義したユーザー設定のデータを読み取ることが可能です。
Global Page

safari.extension.settings.addEventListener('change',function(evt){
	// オプションが変更されたときに呼ばれるイベント
	console.log(evt.key); //変更されたオプションの識別子
	console.log(evt.newValue); //変更後の値
	console.log(evt.oldValue); //変更前の値
},false);
var option1 = safari.extension.settings.getItem('option1');
var secure1 = safari.extension.secureSettings.getItem('secure1');

なお、前述の通り拡張は1つの拡張につき1つのドメインをもっているので、localStorageやWeb SQL Databaseなどを設定データの保存用に使用して、設定用インターフェースをHTMLベースで書く事も可能です(Chrome拡張はこちらの方法)。

パッケージ

さて、ここで一度パッケージしてみましょう。機能拡張ビルダーの右上にパッケージをビルドというボタンがあるのでこれをクリックします。保存場所を聞かれるので適当に選択しましょう。これで、拡張の出来上がりです。将来的にはギャラリーが用意され、そこで配布できるようになる予定ですが、現状は個人のサーバーなどにアップロードして公開することになります。
なお、パッケージすると 表示名.safariextz というファイルが作られます。ちょっと長い拡張子ですね。このファイルはXARという形式で圧縮されたファイルで、Windowsだと7-Zipなどのアーカイバで解凍することができます。

Toolbar Buttons

ツールバー項目について簡単に。機能拡張ビルダーのツールバー項目で「新規ツールバー項目」からツールバーに表示するボタンを作ることができます。このボタンはデフォルトで表示させることもできますが、ユーザーがカスタマイズできるので、必ず表示されるとは限らないので注意が必要です。また、アイコンなので画像が必須です。
ツールバー項目にはいくつか設定がありますが、パレットラベルはツールバーをカスタマイズする際に表示されるボタンの名前です(なのでユーザーにとってわかりやすい名前をつけると良いでしょう)。ツールヒントはマウスを載せた際に表示されるテキストです。イメージはフォルダ内にある画像から選択します。画像は14x14か16x16pxが望ましいそうです。18pxより大きい場合はトリミングされます(あと8bitアルファチャンネルで透過しておけば色んな環境のツールバーになじみやすいとかなんとか)。また、この画像、グレースケールされるみたいです(Windows7でのデフォルトスキンがグレー基調なのでそこで制約されている感じです)。
さて、このボタン、当然ですがクリックされたときなどにアクションを起こすことができます。
Global Page

safari.application.addEventListener("command", function(evt){
	if (evt.command === 'command-name') {
		evt.target.browserWindow.activeTab.page.dispatchMessage('ButtonPush',data);
	}
}, false);
const ID="net.os0x.autopatchwork-LAM47A73AC onoff";
var button;
safari.extension.toolbarItems.some(function(b){
	return b.identifier===ID && (button=b);
});
// someはtrueを返したところで走査を止める配列のメソッド(ECMAScript5で追加)で、
// identifierが一致したところでbutton=bの代入のが行われ、buttonにidentifierが一致した
// toolbarItemが取得できる

// locationからスキームとドメインを取得して絶対パスを作る
// imageに代入できるのは絶対パスのみ
button.image = location.protocol+'//'+location.host+'/icon2.png';
// badgeに数値を代入するとボタンの右上にその数字が表示される(メールの未読件数などを表示できる)
button.badge = 10;
その他

ちょっと長くなってきたのであとは概要の紹介のみに。

  • Extension Bars
    • いわゆるツールバー。HTMLベースで作る。ウィンドウに対して存在するので、同時に複数存在することもある
    • 画面を占有するのであまり使用しない方が良い
  • Contextual Menu Items
    • 右クリックメニュー拡張API。イベントを拾うのはGlobal Page側なので、DOMを見るにはInjecting Scriptsとやり取りする必要がある。
  • Windows and Tabs API
    • Window開いたりタブを開いたり閉じたりするAPI。タブを移動するAPIはない。イベントも現状無い?(タブの選択が変わったことを検知できないので、表示中のタブに応じてツールバーやボタンの表示を変えるのが面倒)
  • Blocking Unwanted Content
    • Injected Scriptから safari.self.addEventListener("beforeload", blockAds, true); みたいなことができる。(Chrome拡張にはない機能)
  • Updating Extensions
  • MIME Typeは application/x-safari-extension みたいです。via A tip for developers | ephemera (Chromeは application/x-chrome-extension Hosting - Google Chrome)
  • Icon
まとめ

繰り返しますがChrome拡張とよく似てます。まあAPIはちょこちょこと使い勝手が違いますが、HTML,CSSの解釈は基本同じ(バージョン間の誤差はある)だし、JavaScriptもほとんど誤差レベルでしか違わないのでSafari拡張とChrome拡張の両方に対応するのはそれほど苦労しないと思いますし、APIの差を埋めるライブラリが出てくるんじゃないかと思います。こうやってHTMLベースな拡張が広まると自然とHTML5を使えるところが増えていくし、拡張から新しいウェブ標準なAPIとして追加されるケースもどんどん出てくるんじゃないかと思っています。個人的にはOperaの拡張サポートに期待したいところです。
サンプルはSafari5の拡張作ってみた - 0xFFに追加していこうと思います。

*1:拡張機能かと思ったら機能拡張でした。日本語としては機能の拡張で自然のような気もしないことはないですが…