tiny hooks

Linux にも Blink Opera がリリースされて以来、結構使っている。Presto Opera と同時に立ち上げて、気分によってどちらかを使っている的な状態だ。あるいは、ナウそうなサイトは積極的に Blink Opera を選択しているかもしれない。Presto Opera では操作不能に陥るくらい重いサイトが Blink Opera ではまあまあサクサクだったりするからだ。

とはいうものの、もちろん Blink Opera より Presto Opera のほうが優れている点も少なくない。例えばキーボードまわりのカスタマイズ性だ。Presto Opera はブラウザ上だろうがアドレスバー上だろうがあらゆるキーボード入力をカスタマイズできるが、Blink Opera にそういう機能はない。

ちなみにそれほどキーバインドを変えまくっているわけではなくて、

  • [cci]c-h[/cci] でページを戻るか、戻れなければページを閉じる
  • [cci]space[/cci] でビューポートの高さの半分 だけスクロールする
  • [cci]j[/cci]、[cci]k[/cci] で 1 行ずつスクロール
  • [cci]h[/cci]、[cci]l[/cci] でタブを切り替え
  • [cci]c-b[/cci]、[cci]c-f[/cci]、[cci]c-n[/cci]、[cci]c-p[/cci] あたりを textarea に対して定義
  • [cci]c-b[/cci]、[cci]c-f[/cci]、[cci]c-n[/cci]、[cci]c-p[/cci] あたりをアドレスバーに対して定義
  • textarea 上での [cci]c-h[/cci] をカーソル前の1文字削除にする

この程度である。かわいいものである。これをなんとかして Blink Opera に持ってきたいのだけど、どうすればいいだろうか。

まず考えられるのは、そういうキーボードコンフィグ系のエクステンションがすでにあるよね、なくても Chrome 版をむりやり動かせばいいよね、ということだ。しかし、問題は、[cci]c-h[/cci] なのである。これの場合、単にキーに機能を割り当てるのではなく、条件判断が必要になる。そういうことを許してくれるエクステンションはあるだろうか? たとえば Keyconfig は許可してくれるだろうか?

しかしどうやら Keyconfig はそういうことはできないようだ(できるのならごめんなさい)。YakShave ならできるかもしれないが、何やらローカルに web サーバを立てる必要があったりとなんだかめんどうそう(ごめんなさい)。

というわけで、もしかして、wasavi に組み込んだほうが早いんじゃねーの!? という気分になりつつある。

play a sound #3

sounds

wasavi の起動時に効果音を出すようにしてみた。

そうすると、当然「そんなのいらねーです!」というリクエストが来ることが考えられる。したがって音を出すかどうかを設定できるとうれしい。

ところで起動時の効果音というのは、wasavi が起動する前に鳴らすわけなので、つまり wasavi 本体が管理する設定([cci]:set[/cci] で制御できるもの)とは別になる。そこで、オプションページで設定することになる。オプションページに効果音のリストとそれを鳴らすかどうかのチェックボックス、そしてボリュームの設定を置いた。

ところで従来は、wasavi が出す音といえばエラー時のビープ音であった。それは [cci]bellvolume[/cci] で設定できたのだけど、ビープ音も前述の仕組みで鳴らすようにしたので bellvolume は obsoleted になった。

play a sound #2

SDK のドキュメントをつらつらを眺めてみると、HiddenFrame なるものを見つけた。これは要するに、バックエンド側で保持することのできる iframe のようなものらしい。HiddenFrame 内に構築される window はごくごく普通のそれであり、Audio コンストラクタも持っている。

そういうわけで、Firefox においては、HiddenFrame 経由で Audio 要素を得ることができた。子供の window で生成したオブジェクトをそのまま親の側で使用できてしまうのがかなり気持ち悪いが、たぶん、きっと、だいじょうぶ。

play a sound

wasavi を実行中、音を出す場合がある。つまりビービー鳴らすモードを持つ vi と同様、必要なときにエラーベルを出す。もし「この音うるせーよ!!」と思われる場合は、ワークアラウンドとして [cci]:set bellvolume=0[/cci] とすれば、とりあえずミュート状態にはなる(例えばこの方がやっている方法だ)。将来的にはビジュアルベルに切り替わったりする気遣いをするかもしれない。

さてこの時に出す音は Audio 要素を使っていて、フロントエンド側で鳴らしている。音源は wasavi 起動時にバックエンドから送られてくる ogg または mp3 データである。

しかし、これは割と無駄なのではないか。複数のタブで wasavi を動かしているとして、各タブがもともと同じデータである音源のコピーを個別に抱えている状態だ。現状は 1 秒未満しか発音しないビープ音なのでデータ自体は数 KB だ。したがってものすごく容量を食っているというわけではないが、やっぱり無駄だ。

これを、Audio 要素をバックエンドで保持するようにして、フロントエンドで音が必要になったら発音リクエストを投げるだけにすれば、そういう無駄は解消できる。ということでやってみて、Opera と Chrome では思ったとおりになったのだけど、Firefox が問題だ。Firefox の Add on SDK 上のバックエンドは、Window ではない不可思議なサンドボックスオブジェクトがグローバルになっていて、つまり Audio 要素をバックエンド側で使えないのだった。

こういう場合、たいてい [cci]Cc[/cci] や [cci]Ci[/cci] といった Firefox 特有の黒魔術をごにょごにょして何とかなったりするものなのだが(Kosian では XHR や Blob や FormData についてごにょごにょしている)、黒魔術はしょせん黒魔術であり、あんまり使いたくない。なにしろ、AMO にアップする際の機械的チェックにすら「おい黒魔術使ってんぞ、気をつけろよな!」などと言われる始末である。禁止されているわけではなく、「気をつけろ」なのがミソではあるのだが。

さて、サウンドプレイヤについてもこの黒魔術を通して Audio 要素っぽいものを得ることはできるようなのだけど

var {Cc, Ci} = require("chrome");
var sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
var uri = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService)
.newURI(self.data.url(...), null, null);

sound.play(uri);

これで得られる nsISound は Audio 要素とは全然異なるものであって、たとえばボリュームの指定とかはできないようなのである。

kosian is here #7

wasavi を構成するコンポーネントとして

  • バックエンド
  • フロントエンド: エージェント
  • フロントエンド: wasavi 本体

の 3 種がある。バックエンドはいわゆるエクステンションのバックグラウンドスクリプト、フロントエンドはコンテントスクリプトなどと言われるものである。エージェントは textarea 要素を持つページにアタッチされて、wasavi を起動させるかどうかの監視などを行う。

これら 3 つが協調することで wasavi が動作する。協調するためにはメッセージングを行う必要がある。

フロントエンドとバックエンド同士のメッセージングは、各ブラウザのエクステンションがその仕組みを提供しているので問題はない。一方で不思議なことに、どのエクステンションも、ドキュメント同士のメッセージング機構はない。

おそらく、ドキュメント同士なら html の規格通りにクロスサイトメッセージングを使ってね! ということなのだろう。でも、[cci]window.postMessage()[/cci] によるやりとりというのは非常に汎用的なものなので、つまりだれでもリスンし放題なのである。怪しげなサイト上で wasavi を起動したりした場合、エージェントに対してクリティカルな情報(たとえば、wasavi のレジスタの内容は更新されるたび同期のためエージェントに内容が通知される)を直接投げるのはちょっとこわい。

ということで、ドキュメント同士のメッセージングとしてはバックエンドを経由してエクステンションのシステムの中で完結するようにした。正確には、もともとそうなっていたのだけど、ここ数ヶ月のソースは [cci]window.postMessage()[/cci] を使うようになっていた。それを元に戻した。

そして、そろそろ各ブラウザのエクステンションストアに置いてある版もここのところのテストが終わったら更新する頃合いだと思う。

kosian is here #6

wasavi のフロントエンドとバックエンドとの通信の方式について若干の変更を行いたい。

そのあたりはもちろん kosian 側で抽象化してあるのだが、wasavi はまず Chrome 上で作り始めたので、Chrome のエクステンションにおけるバックグラウンドとコンテントスクリプトの通信の作法をだいたいそのまま持ってきている。

つまり
extension.postMessage(message [, callback])
というメソッドを用いる。この callback がこの記事の主役である。

この callback を指定することにより、投げたメッセージに対する直接の応答を得ることができる。つまり非常に限定されたメッセージリスナとみなすことができる。一方、フロントエンドはコンテントスクリプト毎の汎用メッセージもリスンしていて:
chrome.extension.onRequest.addListener(handleMessage);
こちらは例えば他のタブで動作中の wasavi で設定が変更されたという同期通知や、dropbox(など)からのファイル読み込みの進捗通知などを扱う。

要するに、2 種類のメッセージリスナを扱っている。この状況で、それぞれが扱うメッセージの性格が完全に分離されているのならば、別に 2 種類あることは悪いことではないのだけど。がしかし上記のファイル読み込みの進捗通知というものが、微妙にどっちつかずな存在なのである。

ファイル読み込みは、フロントエンドからバックエンドへパスなどを渡し、バックエンドで認証などを済まし、読み込むためのリクエストを発行し、その結果をフロントエンドへ返す。フロントエンド側で見るとこの処理は ex コマンド [cci]read[/cci] の処理ということになり、そのハンドラの中で完結するはずのものである。が、実際は [cci]read[/cci] コマンド実行中のバックエンドからのメッセージは、汎用メッセージ側で処理している。これはやや不格好だ。

なぜ postMessage() の callback を使わないのかといえば、つまり [cci]read[/cci] 実行中、バックエンドからのメッセージは複数回送信されるからである。callback は 1 度呼び出されて終わりなのだ。これを拡張し、複数回 callback が呼び出される構造にすれば、必要な処理はすべて [cci]read[/cci] ハンドラの中に掛けば済むようになって、収まりがよいはずだ。

というわけでそうした。ここで、ソースの修正は割とすぐに終わったのだけど、実際に Opera 12 で動かしてみるとさっぱり狙った動作をせず、あーでもないこーでもないとした挙句、単に Opera を再起動させたらちゃんと動くようになったというなんとも言えない事件があったのだが、まあ Opera においては特に珍しいことではないので、いいのだ。

kosian is here #3

kosian ベースの wasavi を Firefox で動かすべくあちこち直している。

Chrome と Presto Opera のエクステンションの仕組みは、もちろん同じではないにしても、いとこ程度には基本的な構造は似通っているので、一方が動けば他方はそれに合わせて直すだけでいいので楽なのだ。

問題は Firefox の Add-on SDK なのである。このプラットフォームで作る拡張機能というのは、かなり異質だ。Chrome や Opera の遠い親戚の、隣に住んでいるバングラデシュ人くらい異質だ。

Add-on SDK でいわゆる content script 的なものを動かすには、PageMod API を使う。このとき、どの URL で content script をアタッチし、あるいはどの URL ならアタッチしないかという include と exclude の情報が必要になる。wasavi の場合フロントエンドは agent と wasavi 本体にコードがわかれていてアタッチの仕方が若干複雑なので、exclude 機能は必須である。で、どちらを指定する機能も、Chrome と Opera は当然持っている。

Add-on SDK の PageMod は include の指定しかできないのである。意味が、わからない。そして、最新のドキュメントを見ると exclude 機能は Firefox 32 で実装されるよ! 乞うご期待! という扱いになっているのである。意味がわからない。

どうするか。include には URL がマッチすべき文字列か正規表現オブジェクトを渡すことになっている。ここで PageMod が求める正規表現オブジェクトとは必ずしも RegExp のインスタンスである必要はなく、test() と exec() メソッドを備えていてそれぞれがそれぞれの働きをすれば通してくれるので、今まで wasavi ではそのように振る舞う独自のオブジェクトを渡していた。この時点でちょっとアクロバティックである。

で、Firefox 30 でそのコードを動かしてみると見事に動かない。なんと include に渡されたものが文字列か RegExp インスタンスかどうかのチェックを行うようになったのである。このチェック自体は正しい。しかし前述の通り Firefox 30 の PageMod には exclude 機能はない。意味がわからない。どうしろというのか。

仕方がないのでさらにアクロバティックなことをして(互換性はないのに constructor を RegExp にする)動かすようにしたのだが、もちろんかなり嫌なコードである。なお、さらに PageMod 内部で摩訶不思議な RegExp インスタンスのキャッシュ機構が働いているらしく、toString() も上書きしないとダメだったりする。本当にがっかりさせられる。

最初から exclude 機能をつけてくれればこんな変なことをする必要はないのだし、そもそも include 機能があって exclude はないという API のデザインセンスの意味がわからない。

こういう記録が残るブログであまり穏やかではないことは書きたくないのだけど、Add-on SDK チームってバカなんかじゃないかと思う。

hello, onedrive

OneDrive にも対応するようにした。OneDrive というのはかつては SkyDrive とよばれていた、Microsoft が提供するオンラインストレージだ。

そのためにドキュメントをいろいろ参照するわけなのだけど、まあいつもの MSDN クオリティというか、量自体は豊富なんだけど痒いところに手が届かない感じがしてあんまり使いやすくない。

あと、OAuth での認可を受けるためのエンドポイントのドメインが login.live.com なのに OneDrive(というか Live Connect)の API は apis.live.net だったりちぐはぐなのがなんか全体的にこう……なんと言ったらいいのか MS らしいですね!

かつて apple の親分だったおじさんが MS を評して曰く、「彼らはセンスが足りないんじゃない。『ない』んだ」とかなんとか、と聞いたことがあるけれど、もしかしたら、まったくもって、その通りなのかもしれない。

OAuth 2.0 in a browser extension

アプリケーションが OAuth 2.0 における認可を得る際の手順は、そのアプリケーションの性格によっていくつかの系統に分かれる。このあたりは google のドキュメントなどにまとまっているのだが、つまり

  • Web サーバが主体になる場合
  • ネイティブアプリケーションが主体になる場合
  • ブラウザが主体になる場合

と大別される。いずれにしても、基本的には

  1. 認可を得るための一時的なコードを得るための最初のリクエスト(これは正確には認可を得るためのページを web ブラウザで開くだけの処理だ。認可を得るかどうかはそのページを見ているユーザが判断する)
  2. そのコードをアクセストークンへ交換するための 2 度目のリクエスト
  3. 得たアクセストークンが正しいものであるかの確認のリクエスト(これは認可を得るための必須の処理ではないけれど、普通は行う)

を順に行うという流れに従うのだが、何が主体になるかで微妙なところが微妙に違う。具体的には、一時的なコードを得るための経路や、アプリケーションごとに割り当てられるクライアント ID、クライアントシークレットの扱いなどだ。

  • Web サーバが主体になる場合、リクエスト 1 の際にリダイレクト URL を指定する。この URL として、Web サーバの管理が及ぶ適当なものを与える。アプリケーションを利用するユーザが承認すると、ユーザが見ているブラウザのページは自動的にリダイレクト URL に転送される。このとき、URL に追加されているクエリ文字列に一時コードが含まれているので、Web サーバはそれを得ることができる。
  • ネイティブアプリケーションが主体になる場合、Web サーバのようには行かないので、2 つの特殊なリダイレクト URL のどちらかを使用する。[cci]urn:ietf:wg:oauth:2.0:oob[/cci] を指定した場合は、何やら呪文のようなアドレスだが、つまりリクエスト 1 の時点で単にページ上に一時コードを表示して一旦終了ということだ。これをユーザが手作業でアプリケーションにコピペする。[cci]http://localhost[/cci] を指定した場合は、あらかじめネイティブアプリケーションがローカルマシンの適当なポートをリスンしておき、その経路を通して一時コードを得る。
  • ブラウザが主体になる場合は特殊で、一時コードをすっ飛ばしていきなりアクセストークンを得ることができてしまう。ただしこれで得たアクセストークンは非常に短い有効期間しか持っていない。

と、ここまでは、上のドキュメントに書いてあることそのままなので、いいのである。問題は、ブラウザに組み込まれるエクステンションの場合はどのシナリオを選択すればいいの? ということだ。

エクステンションは単なるブラウザ上のスクリプトよりも多くの機能を持っているので、ブラウザだからといってブラウザ主体のシナリオのみ許されるわけではない。それどころか、とても興味深いことに、ブラウザのエクステンションは工夫次第によって 3 つのシナリオを全てを実装できてしまう。そして同時に、いずれの方式にも若干の気をつけるポイントも出てくる。どれを選択するべきなのか?

 * * *

Web サーバシナリオを選択した場合、適当な外部の Web サーバを用意し、リダイレクト URL を作る。エクステンションはリクエスト 1 によるアクセスにまつわるブラウザのページ遷移を監視し、リダイレクト URL に達した時点でクエリ文字列を得ればいい。これに嫌な点があるとすると、一時コードを含んだ URL が認可サーバからブラウザに来るパスの他にもう一度、ネットワークを流れてしまうということだ。そのためリダイレクト URL を SSL 経由にしたりする必要があるが、これはなかなかコストのかかる作業である。

ネイティブアプリケーションシナリオを選択した場合、外部 Web サーバは不要だ。ブラウザのリダイレクトを [cci]oob[/cci] にし、ページに一時コードが表示された時点でエクステンションからそれを読み取ればいい(このへんがちょっとハックでストレートじゃないのが気になるが)。一方で、これは Web サーバシナリオでも実質的に同じだが、クライアント ID とクライアントシークレットをエクステンションのパッケージに内包させなければならないのが気になる。全然シークレットでもなんでもなくなってしまうのである(これは、上のドキュメントにも書いてあることであるが、ネイティブアプリケーションシナリオにおいてはどうにもならない副作用だ)。

wasavi は dropbox のクライアント情報に関しては、エクステンションのパッケージ内に暗号化して含めるようにしている。これはプレーンな状態で含めようとしたら dropbox の審査で暗号化しろよ! と怒られたのでそうした経緯があるのだが、しかしまあ、ブラウザのエクステンションである。すなわち javascript コードがそのまま入ってるのである。コードを読めば、どう復号するのかはすぐわかっちゃうわけで、意味があるのかどうかはいまいちわからない。

最後のブラウザシナリオでは上に書いたような問題点は一見ない。アクセストークンを得るためにクライアントシークレットは不要だ(クライアント ID は必要なのでこのあたりの問題が根本的に解決するわけではない)。しかしこの形式で得たアクセストークンは前述したとおりすぐ切れる。wasavi では Google Drive との接続はこの方式を用いているのだが、だいたい 1 時間もするとすぐに再認可のページに飛ばされてしまう。これは使い勝手が良いとはちょっと言えない。

hang on search?

issue #20 で、検索するとパターンによって無限ループになってタブを閉じるしかなくなるというのがある。特に [cci]|[/cci] とかが危険らしい。なるほどなかなかそんな雰囲気がしないでもない。

これは由々しき問題だ……ということですぐ直そうとしてみたのだけど、再現しない。ということでどういうパターンだと発生するのか聞いているところ。