to open a hole #2

結局昨日の記事とはぜんぜん違う方法になりつつある。

まず wasavi 側は、コマンドが終了するたびにエージェント(wasavi が寄生する textarea 要素が属する文書に対して挿入される content script)にそれを通知する。通知の際に wasavi の状態も併せて送りつける。

エージェントでは、文書上の wasavi の実体である iframe の適当な属性に、wasavi の状態を json 文字列化した上で格納する。

テストページでは、呼び出されるたび iframe に格納された wasavi の状態を単に返す関数 getWasaviState() を定義する。

Selenium 側では、wasavi へキーストロークを送信した直後、コマンドが終了するのを待ち、終了したら getWasaviSatate() を呼び出し、結果を取り込む。

という流れ。

 * * *

全然関係ないけど Thinkpad X1 carbon が発売開始された。いいなー欲しいなー誰か買ってくれないかな。

to open a hole

wasavi.js の動作テストを行う際、起動中の wasavi の状態を外から取得する必要がある。

そのために、wasavi はグローバルオブジェクトに対して Wasavi オブジェクトを公開している。それには Wasavi.value とか Wasavi.state、あるいは Wasavi.registers() といったものが用意されていて、外から状態を得ることができる。

これが可能なのは、jsUnit でテストするときは wasavi.js をページに属するスクリプトとして扱っているからだ。つまりそのコンテキストでのグローバルオブジェクトは window であって、外から見るのに何の制限もない。

さてテストの方法を Selenium を利用したものに移行しようとしているのだが、こちらの場合は実際にエクステンションとして動作する wasavi に対してテストを行う。この状態では、上記の Wasavi オブジェクトは外から見えないし、また見えては困る。しかしテストを行うためにこれに例外を設ける必要がある。

テストページとして、http://wasavi.appsweets.net/test_frame.html を用意している。Selenium はこのページを自動的にブラウザに開かせ、すべてのテストを行う。Selenium、このページ上のスクリプト、wasavi の 3 つの要素が連携して、制限されたパスを通して wasavi の状態を Selenium へ伝達したい。

まず Selenium 側。Selenium 側はつまり java である。Selenium の api に executeScript() というものがあるので、これを利用する。このメソッドはブラウザに対して任意の javascript を同期的に実行させ、値を返す。スクリプトはブラウザ上では匿名関数の中身として実行される。

JavascriptExecutor js = (JavascriptExecutor)driver;
Object result = js.executeScript("return getWasaviProperty('value');");

次にテストページ上のスクリプト。ここから起動中の wasavi へ何かしらの操作をし、何かしらの状態を取得することになる。wasavi の実態は iframe であり、テストページと同じドメインに存在する。しかし、いずれのブラウザでも wasavi.js は isolated world に存在しているので、単純に iframe#contentWindow を覗けばいいというふうにはいかない。

そこで、テストページから wasavi へ postMessage() することにする。また、wasavi 側からの返答を受けるために message イベントハンドラを登録しておく。

var buffer;
function getWasaviProperty (name) {
$('wasavi_frame').postMessage(name, 'http://wasavi.appsweets.net');
return buffer;
}
window.addEventListener('message', function (e) {
buffer = e.data;
}, false);

最後に wasavi 側。テストページからのメッセージを受け、値を返す。

window.addEventListener('message', function (e) {
e.source.postMessage(Wasavi[e.data], e.origin);
}, false);

なお上記コードはエラー処理とか省いている。特に origin の判定とかは実使用では絶対に必要なので注意。

というわけでとりあえず Chrome で動かしてみたのだけど、前にも書いたような気がするが、どうもやはり、ページ上に属する javascript と content script の間では cross messaging すらできないような。具体的には、wasavi 側でメッセージを受けるところまではいくのだけど、MessageEvent#source が undefined なのだ。

困ったー。

 * * *

iframe#contentWindow に対してではなく、MessageChannel に対してなら postMessage できるようだ。どうしてそういう動作をするのかはよく分からない。

yet another vi clone #3

textarea 要素を vi として振る舞わせるエクステンション。ただし、現在はメンテされていないものが多い。特に Firefox は 3.6 あたりで軒並み古いエクステンションを切り捨てているようなので、公開した当時のままのエクステンションは分断されている。

いずれも textarea に対して直接キーボードイベントハンドラを仕掛けて、自前の制御を行っている。また、ex コマンドに類するものはほとんど実装されていない。

review rejected #3

Firefox 版 wasavi のレビューがなされた。もちろん rejected。

まず innerHTML 使うなボケ! ということである。wasavi.js では大まかに分けて 3 箇所、innerHTML を使っている箇所がある。ひとつめは、wasavi 本体の iframe 内で、

document.body.innerHTML = wasaviFrame;

として wasavi の骨組みを組み立てている。ここで wasaviFrame には、body 以下の骨組み全体となる html の文字列が入っていて、バックグラウンドから送られてくる。バックグラウンド側ではこの文字列は、エクステンションのアーカイブ内から読み出す。

この innerHTML の操作を DOM のメソッドを使用して要素ずつ組み立てる処理に置き換えるのは、ちょっとコストが高すぎる。また、前述の通り内容が外部から操作されるというものでもない。したがってこの箇所については、innerHTML の使用は問題ないはずだ。そういうことを説明することにしよう。

ふたつめは、wasavi が拡張する対象となる textarea 要素の value を wasavi へ複製する際。

var html =
'

' + textarea_value
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/\r\n/g, '\n')
.replace(/\u007f/g, '\u2421')
.replace(/[\u0000-\u0008\u000b-\u001f]/g, function (a) {
return String.fromCharCode(0x2400 + a.charCodeAt(0));
})
.replace(/\n/g, '\n
') +
'\n

';
this.elm.innerHTML = html;

こんな感じ。こちらは、textarea_value に入ってるものがページとユーザ次第のものなので、確かに innerHTML 経由だとセキュリティリスクが生まれる余地がある(上記の事前のエスケープを回避する方法はちょっとすぐには思いつかないが……)。というわけで、こちらは \n で分割した textarea_value を逐一生成した div の textContent に突っ込むようなループに変更した。これはテキストが 1 万行とかになるとパフォーマンスの問題が出てくるかもしれない。

最後に、さまざまな要素の内容を生成する際に

elm.innerHTML = '';

とやっている箇所がいくつかある。これにはセキュリティリスクが入り込む余地はないと思うが、DOM Range を用いて内容を消去するように変更した。

function emptyNodeContents (node) {
var r = document.createRange();
r.selectNodeContents(node);
r.deleteContents();
r.detach();
}

innerHTML 関連は以上。

次に、javascript 内で他の javascript ソースを読み込むのに

document.write('