たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
GoogleChromeの拡張を作る上でFirefoxアドオン作者が知っておくべきやればできること « kuでFirefoxアドオンの自由度は奇跡的です。millennium | Google Chrome Extensionsを読んでその奇跡を噛み締めましょう。
と紹介されていたエントリを勝手に翻訳してみました。原文の著者は、Firefoxの初期開発チームの一員で、今はGoogleに転職してGoogle Chrome開発チームの一員となっているBen Goodger氏です。
Chromeチームは今日(※訳注:2009年12月8日)、Mac OS XとLinux、そして拡張機能に対応した最初のバージョンをリリースしました。これは、非常に多くの人達の1年間にわたる作業の完成を意味しており、私は開発チームがこの目標を達成できたことを誇らしく思っています。そこで、拡張機能について少し語る機会を設けることにしました。
Chrome拡張機能APIはユーザと開発者に対して、Chromeの核となる原則である「速度」「安定性」「安全性」そして「シンプルさ」を保ちながらブラウザをカスタマイズする能力を提供します。
Google Chromeの拡張機能には以下のようなことができます。
Google Chrome拡張機能についての動画はここで見られます。
私はここしばらくの間、ブラウザの開発に関わり続けてきました。私の話を興味深く思う人もいるでしょうから、拡張機能の歴史について少しお話ししましょう……
かつてNetscapeがNetscape 6(ブラウザ、メールクライアント、ページ作成ツール、そしてIMクライアントのセット)を開発していた頃の要求仕様の1つとして、メールクライアント、IMクライアント、ページ作成ツールといった「選択式の」コンポーネントを含めずにブラウザだけをインストールできるようにする、というものがありました。それらのコンポーネントをインストールした場合は、メールクライアントやIMクライアント等を起動できるようにするために、ブラウザのUIに「追加の項目(メニュー項目、ツールバー上のボタンなど)」が表示されることになります。
NetscapeのフロントエンドはクロスプラットフォームなUI言語であるXULによって実装されていました。選択式のコンポーネントは、それらがインストールされている場合に「追加の項目」を加えられるよう、「オーバーレイ」と呼ばれるファイルを含んでいました。選択式のコンポーネントはそれ自身のオーバーレイや他のロジックを「XPI」ファイルの中にパッケージングされていて、インストールエンジンによってインストールされるようになっていました。
何年か後、Netscapeの後継者であるFirefoxの人気が高まると同時に、開発者達は、前述のようなコンポーネントのインストールのための仕組みを、ブラウザに対して他の機能を追加するためにも利用できるという事に気がつきました。この機能自体はNetscapeの自社製品以外が使うことを全く想定せずに作られていたので、ブラウザのUIの中でどんなAPIが公開される必要があるのかといったことに対しては十分な注意が払われていませんでした。本当に単なる内部的なAPIでしかなかったのです。そのため、人々がFirefox用の「拡張機能」の開発を始めたばかりの頃は、Firefoxに何か変更が行われるとその度に拡張機能のせいでブラウザが起動しなくなるというのが日常茶飯事でした。Firefoxの初期の頃には、ある拡張機能(※訳注:多分タブブラウザ拡張のことじゃないだろうか。というのは自意識過剰?)をインストールしていたユーザにとっては、Firefoxの新しいバージョンにアップグレードする度に「ブラウザにXBLのバインディングが適用されていません」というエラーにぶつかる、といったことも珍しくありませんでした。
Firefoxがバージョン1.0に達する前にこの問題を解決しなければならないのは明らかでしたので、私は拡張機能マネージャをFirefoxに加えることにしました。これは、インストールやアンインストールのためのやっかいなコードを書かなくても済むようにすると同時に、より重要な点として、バージョン管理のための仕組みを提供するものでした。安定したAPIが無い以上、システムは単純に、ブラウザのそのバージョンに対して互換性があると明記されていない拡張機能を無効化します。これは開発者達にとって、新しいバージョンのFirefoxがリリースされるごとに毎回、彼らの拡張機能の動作を保証しなければならないということを意味します。この仕組みは完璧でもないし、煩わしいものでしたが、しかしそれなりにうまくいったため、私達はひとそろいのAPIを凍結させなくてもよかったのです。APIの凍結を急かされていたら、まず間違いなくそれはおざなりな物になってしまっていたでしょう。
Firefoxの拡張機能は常に諸刃の剣です――それは計り知れない柔軟性を提供しますが、その引き替えに、ブラウザがアップデートされるごとに毎回動作確認を取らなければいけないというコストを開発者に対して強います。開発者の動作確認が遅れた場合、ブラウザの新バージョンがでてすぐにアップグレードしたユーザは、しばらくその拡張期能なしで過ごさなければならないか、あるいは、互換性の確認の過程を自己責任で無効化する必要があります。
私はこの歴史についての覚え書きには、面白いというだけでなくケーススタディとしても価値があると考えています。もし、Netscapeのコンポーネント式のインストールのおかげでこのブラウザのカスタマイズのための「裏口」が存在していなかったら、Firefoxに拡張機能の存在自体が全く無かったかもしれないのです。考えてみて下さい。その機能がユーザに愛されたために、Firefoxは成功したのです。これは驚異的な事です。この前例があって、ユーザが拡張機能を愛しているということが分かったので、私達Google Chromeチームは今、カスタマイズ性とChromeの核となる原則とを両立させる最良の物を作るべく、新しいAPIをゼロから作るという贅沢が許されているのです。
Firefoxでの体験は計り知れないほど有益な物でした。私は、Google Chromeの拡張機能の開発に個人的に寄与した数多くの技術者達のうちの1人ではありませんが、しかし、機能をどのように実現するかを検討する事については十分な戦歴があります。私はChrome拡張機能のチームがこのアプローチを取ることを決定した事をとても嬉しく思います。最初のマイルストーンへと私達を導いてくれた彼らに賞賛を!
以下、Adblock Plus and (a little) more: Five wrong reasons to use eval() in an extensionのいいかげんな訳です。XUL/Migemoのバージョンアップ時のエディタによるレビューのコメントで「今回は公開を承認するけど、次からはeval()
はなるべく減らすように。詳しくはこれを読んで。」と指摘されたので、自分が読むために訳してみました。誤訳があったら指摘して。一部のサンプルコードは見やすさのためにインデントを勝手に加えてます。
ちなみに、僕は5番目の点(こういう用途でeval()
を使うなという話)については反対の立場です。拡張機能同士を協調して動作させたいなら、むしろeval()
を使って関数を書き換えるやり方を使う方が望ましいとすら考えています。なので参考のために、似たような立場と思われるSimon氏・Dorando氏のコメントも訳しています。
2010年2月8日追記。このエントリで述べられている内容に対する反論を公開しました。できればそちらも併せてご覧下さい。
過剰に使われているJavaScriptの機能のひとつに、eval()
関数があります。私はそれが非常に多くの拡張機能で利用されているのを見てきましたが、そのうちのごく一部だけが正しい使われ方をしています。ですので、eval()
のすべての間違った使い方について説明したいと思います。
今日において、JSONはデータを保存するためのポピュラーな形式となっています。その最大の特長は、パースが非常に簡単であるということです。単に data = eval(json)
という風に書けば、それだけで事足ります。
うまい話には裏があるんじゃないの? その通り。このjson
という変数は {foo: "bar" + alert(Components.classes)}
のような内容が含まれるかもしれず、このようなJavaScriptのコードを実行してしまうと、あなたが意図していなかった結果になってしまうでしょう。このように、信頼できない情報源からやってきたデータをJSONとしてパースする用途にはeval()
は全く不向きです。それがFirefoxの拡張機能であるなら、どんなWebサーバから送られてくるデータも信頼できません。もしそれがあなたのWebサーバであっても、ハックされて(※訳註:原文ではhacked
)いるかもしれませんし、ユーザへの通信経路上で情報が改竄されているかもしれません(特に、暗号化されていない接続では)。あなたは、ユーザを危険な状況に晒したくはないでしょう。
それだけではありません。そのデータが拡張機能自身によって(例えば、ブラウザ終了時の状態を保存するためなどの目的で)書き出されたデータであっても、常に信頼できるとは限りません。その中にはひょっとしたら、Webから受け取ったデータが含まれるかもしれません。もしJSONを書き出す処理にバグがあって、JavaScriptの文字列として書き出すべき物が文字列になっていなければ、それをJSONとして解釈しようとした時、知らない間にJavaScriptのコードとして実行されてしまうでしょう。これが、JSON処理専用の機能を必ず使うようにした方が良い理由です。JSON処理専用の機能は、不正なデータを受け取った時にもJavaScriptのコードとして実行してしまわないので安全です。
obj.fooN
のNがn
という変数の値である、というプロパティにアクセスしたいとき、どんなコードを書けばよいでしょうか? これは、あなたがアクセスしなければいけないプロパティの名前が事前には分からなくて、動的に決定されるものであるという場合のことです。拡張機能の中には、これを eval("obj.foo" + n)
のようなやり方で解決しているものがあります。この時、その拡張機能はn
の値の中に危険な内容が含まれていないかを検証する必要があるでしょう――でも、どうやって?
幸いにも、この質問の答えを考える必要はありません。もっといい方法があります。JavaScriptではすべてのオブジェクトが連想配列である(※訳註:原文ではassociative arrays
)ことを思い出してください。言い換えると、obj.foo
と obj["foo"]
は全く同じ意味で、すべてのプロパティは配列の要素としてアクセスできるのです。ですから、前述のような問題を解決するには単に obj["foo" + n]
とだけ書けばよく、この操作は、何も他の余計なことをすることなく常にそのプロパティにアクセスするでしょう。
では、メソッド(関数)の場合は? JavaScriptではメソッドも、値が関数オブジェクトであるという違いがあるだけのただのプロパティです。その関数を this
が正しい値を示すようにして呼び出すために、Function.call()
というメソッドが利用できます:
var method = obj["foo" + n];
method.call(obj, param1, param2);
あるいは簡潔にこう書くこともできます:
obj["foo" + n](param1, param2);
同じアプローチが、グローバル変数やグローバルな関数に対しても使えます。「グローバルオブジェクト」のすべてのプロパティはwindow
のプロパティとして参照できます。window.foo
や window["foo"]
は、グローバル変数foo
の値を返すでしょう。(※訳註:JavaScriptコードモジュールなどwindow
が使えない変数スコープでも、 (function() { return this; })()
とすればその実行時の変数スコープのグローバルオブジェクトを取得できます。)
私が時々見かけるひとつのパターンは、このような関数の呼び出し方です:
foo("window.close()");
その関数は他の場面で、異なるJavaScriptのコードをパラメータとして渡されていました。そして関数が処理を終えた後で、パラメータとして渡された内容を動作の指定として eval()
で実行するようになっていました。
どう見ても、ここにはセキュリティ上の問題はありません(※訳註:もちろん、パラメータで渡す内容にWebから取ってきたデータが含まれる可能性がある時は問題外ですよ!)。では、このアプローチの一体どこが間違っているのでしょうか? 実際には、以下のような問題があります:
eval()
が呼ばれるまでコンパイルされないでしょう。これは、それ以外の部分のコードについてはスクリプトが読み込まれた時にすぐにJavaScriptインタープリタが文法エラーを報告するのに対して、関数のパラメータとして渡されたコードの文法エラーは後になってからしか報告されないため、そのコードが実行されるような経路を辿る操作をあなたがテストしなかった場合に、問題が見過ごされてしまうだろうということを意味します。foo()
に対して実行して欲しいコードをパラメータとして渡すというのは普通のやり方ではなく、見苦しい回避方法を色々と必要とします。(※訳註:ダブルクォーテーションをエスケープしないといけない、など。)幸いにも、クロージャを使うことによってそれらの問題は解決できます(※訳註:この例はクロージャではなく関数リテラルとか関数オブジェクトとかその辺の話だと思うんですが……)。以下は、前述のコードを少し書き換えた例です:
foo(function(error)
{
alert(error);
window.close();
});
そしてfoo()
という関数の内容は以下のようになるでしょう:
function foo(callback)
{
...
callback("Full success");
}
以下のようなボタンがあると仮定しましょう:
<button id="button" oncommand="doSomething();"/>
このイベントハンドラを実行するために eval(document.getElementById("button").getAttribute("oncommand"))
としてはいけないのは何故でしょうか? その要素が実際にクリックされたなどの場合以外の所でイベントハンドラを実行するための方法として、拡張機能の中ではしばしばこのようなやり方が用いられます。しかしながら実は、commandイベントを生成する方法のほうがもっと簡単で、しかもイベントハンドラがどのように定義されていようとも正常に動作することが期待できます:
document.getElementById("button").doCommand();
doCommand()
というメソッドは、すべてのXUL要素で利用可能です。他のイベントに対しては、document.createEvent()
を使って本当のイベントオブジェクトを生成する方がよいでしょう――何故なら、イベントハンドラがそれを期待しているでしょうから。例えば:
var event = document.createEvent("MouseEvents");
event.initMouseEvent("click", true, true, window,
0, 0, 0, 0, 0,
false, false, false, false,
0, null);
document.getElementById("button").dispatchEvent(event);
では、あなたが「onfooaction」という風な独自の属性を定義していて、それがいかなる実際のイベントとも関連付けられていない場合には? このような場面でも、eval()
を使うのは最良の選択とは言えません。何故なら、eval()
を呼び出した関数の実行コンテキストにおいてコードが実行されてしまうからです。もしそのイベントハンドラがfoo
というグローバル変数を参照したとして、あなたがそのイベントハンドラを呼んでいる関数の中にfoo
という名前のローカルな変数があったら――そのイベントハンドラは意図しないままにそのローカル変数にアクセスしてしまうでしょう。そしてもちろん、その時イベントハンドラに対してパラメータを渡すこともできません。よりベターな解決策は、そのイベントハンドラのための関数を作る事でしょう:
var handler = new Function("param1", "param2",
document.getElementById("button")
.getAttribute("onfooaction"));
handler("foo", "bar");
このシナリオでは、このイベントハンドラは「foo」をparam1
という名前の引数として、「bar」をparam2
として受け取るでしょう。(これは、よくあるインラインで記述されたイベントハンドラに対してevent
というパラメータを渡す時にも使えます。)
以下のようなことをしているコードをよく見かけます:
gBrowser.foo = eval(gBrowser.foo
.toString()
.replace("foo", "bar"));
このようなやり方でブラウザの関数を書き換えている人は、公の場所でおしりペンペンされることをお勧めします。それは、ブラウザの関数を新しい関数で単に置き換えてしまうだけの拡張機能に比べてほんのちょっとだけマシであるに過ぎません。どちらの場合も、書き換えられようとしているコードが変化しないことを前提にしていますが――しかし、もしそれが起こったら? 最良のケースでは、その拡張機能は大きな損害を与えることなく単に動作しなくなるでしょう。しかしひょっとすると、ブラウザを壊してしまうかもしれません。あるいは、ブラウザのその関数がセキュリティ上の問題の修正のために書き換えられたとしたら、その拡張機能は同じセキュリティ上の問題をまた持ち込んでしまうかもしれません。
言い換えると――この使い方はしてはいけません。ほとんどの場合で、このアイデアはブラウザの関数の動作を変えるのではなく、その関数の前後に追加の処理を挿入するために使われています。幸いなことに、それをするためのもっと危険でないやり方として、ここでもクロージャを使えば元の関数を単純にあなたの関数で包み込むことができます:
var origFoo = gBrowser.foo
;
gBrowser.foo
= function(param1, param2)
{
if (param1 == "top secret")
doSomethingBeforeFoo();
var result = origFoo.apply(this, arguments);
if (result == null)
doSomethingAfterFoo();
return result;
}
元の関数にすべてのパラメータを渡すためにFunction.apply()
を使うことに注意してください。その関数が今現在2つだけパラメータを受け取っているとしても、将来のバージョンのブラウザでは変わるかもしれません。あなたの拡張機能は新しいパラメータに対して何をすればよいのか知っているかもしれませんが、元の関数の動作を壊さないために、それらの新しいパラメータはそのまま元の関数へ引き渡しましょう。
eval()
の正しい使い方とは?私は、eval()
関数の有効な使い方はそれほど無いと思っています。拡張機能の中にはユーザに対して、評価可能なJavaScriptのコードを入力させることを許しているものもあります。そのスクリプトが関数のパラメータとして値を受け取る必要があり、関数を作り変数を渡すのにFunction()
コンストラクタを使うことが依然として望ましいとしても、これはeval()
の妥当な使い方でしょう。
もう1つのeval()
の使い方として、状況に応じて定数を宣言するためにも使えます:
if (typeof MY_CONSTANT == "undefined")
eval("const MY_CONSTANT = 'foo'");
こうすることによって、もし他のスクリプトで同じ名前の定数が定義されていても、あなたは文法エラーを目にしなくて済むでしょう。しかしながら、私はこれはその場しのぎのやり方だと思います。もし同じ名前空間で実行される未知のスクリプトと衝突することを恐れているのなら、あなたは他のスクリプトが使わないような一意な名前を定数(グローバル変数も)に与えるように気をつけるべきです。また、あなた自身のスクリプトについても、定数宣言を含んでいるスクリプトを複数回読み込まないように気をつけてください。
最後に、実行時にそれ自身のコードを生成するためにeval()
を大量に使う、分かりにくい・「圧縮」されたスクリプトもあります。Webにおいては「圧縮された」スクリプトに価値があることは認めますが、拡張機能の中で同じ事をやることにはほとんど意味がありません。拡張機能は1度だけしかダウンロードされませんから、ダウンロードにかかる時間をたった2秒だけ節約できても、誰も喜ばないでしょう。また、「圧縮」されたスクリプトはロードされ実行される度に毎回、処理に余計な時間を食うことでしょう。
5番目について。関数の中のコードに手を入れる(単にコードの前後に処理を付け加えるだけでは実現不可能で、関数全体を書き換えないといけないような場合)方法として、どんなやり方なら勧められるというのでしょうか? また、ある1つの拡張機能があなたの推奨しているやり方を実行したら、関数の中身を書き換えるタイプの他のすべての拡張機能の動作が妨げられ、それらの拡張機能はすべてのコードを書き直すことを強いられるので、拡張機能同士が円満に共存できなくなるでしょう。
私は元々はあなたが勧めているようなやり方を取っていましたが、しかしeval()
を使うことによってしか解決できないような非互換性の問題に何度か遭遇してきました。より良いやり方があるのなら私は喜んでそれを採用するつもりですが、残念ながら、私にはあなたが勧めているやり方が問題を解決してくれるようには見えません……
4番目について。new Function("some code")
はeval("some code")
と比べてどのように安全なのでしょうか? bug 477380であなたはeval()
を禁止することを提案していますが、new Function()
も禁止しないと無意味なのではないでしょうか。
あなたが書いたわけではない関数の中身に対して変更を行う事は、一般的に言ってよいアイデアではありません。あなたがどんな風にそれをやったとしても、必ず酷い結果になるでしょう。他の手段(例えばObject.watch()
など)であなたがやりたいことを実現できないのであれば、多分あなたはそれをするべきではないのでしょう。
new Function()
について。それはeval()
に比べて本質的に安全であるとは言えませんが、それはたいていの場合静的なコードのために使われ、それほど多くの問題を持ち込みません。これは、トラブルを抱え込まないようにするための良いコーディングの習慣ということです。
特権が無いWebページの場合でも同じ事が言えるのでしょうか? Gecko 1.9.1はグローバルなJSONオブジェクトを実装するそうですが、evalInSandboxと同等のものはあるのでしょうか?
evalInSandboxを使うためにUniversalXPConnect権限が必要なのは困ります……
クロスサイトスクリプティングを防ぐことについて話しているのだと思いますが――スクリプトのためのサンドボックスは十分な対策とは言えません。これについてはbug 341604(※訳註:IEの独自拡張であるiframe要素のsecurity属性の実装についてのバグ)とWHATWGのWeb Appsの仕様のiframe要素のsandbox属性を参照してください。
5番目について、例えば元の関数 gBrowser.foo
について以下の場合を仮定しましょう(似たようなコードはMozillaのコードベースから簡単に見つけられます。例えばtabbrowser.xmlで「.isTrusted」や「permitUnload」を検索してみてください):
gBrowser.foo
= function(param1, param2)
{
if(!gBrowser.isItSaveToDoSomethingBeforeFoo())
return;
if (param1 == "do_nothing")
return;
var color = "red";
doSomething(color, param2.length);
if (param2 == "some_extensions_want_to_prevent_this")
doSomethingAnnoying("dark"+color);
}
doSomething()
が実行された時に、常にdoSomethingAfterFoo()
を実行するようにしたい、という時はどうすればいいのでしょうか?doSomething()
が実行されるであろう時に、常にdoSomethingBeforeFoo()
を実行するようにしたい、という時はどうすればいいのでしょうか? 元のコードのうちいくらかを複製することはできますが、それは既に修正されたセキュリティの問題をまた持ち込みかねず、関数を単に書き換えるだけにしない事のメリットを打ち消してしまいかねません。関数に対して行われる変更を常に参照し続けることは、いくつかのシチュエーションでは上手く行くでしょうが、私に言わせてもらうとそれは大変すぎます。(そしておそらくコードをいくらか遅くさせるでしょう。)gBrowser.foo
がdoSomethingAnnoying()
を実行するのを食い止めたい時はどうすればいいのでしょうか?gBrowser.foo
が使う色をgreen
に変えたい時はどうすればいいのでしょうか? 追加の関数を置き換えてその呼び出し元関数を確認するようにするという手もありますが、私にとってはそれは何かを壊してしまう可能性を増大させるものでしかないです。関数に本来の処理を行わせた後でその変更を後から取り消すというやり方も、可能ではありますが、良いユーザ体験をもたらすものとは思えません。どちらの場合も、書き換えられようとしているコードが変化しないことを前提にしていますが――しかし、もしそれが起こったら?
maxVersionはそのために存在しています。
あるいは、ブラウザのその関数がセキュリティ上の問題の修正のために書き換えられたとしたら、その拡張機能は同じセキュリティ上の問題をまた持ち込んでしまうかもしれません。
isItSaveToDoSomethingBeforeFoo
が導入されて、doSomethingBeforeFoo();
がeval()
でパッチを当てられた場合と同様の内容を含むようになったとしたら、(※訳註:ここで推奨されているやり方の方の)関数の置き換えでも同じ問題が起こり得ます。
その一方で、拡張機能の作者はeval()
によって、(セキュリティに関するものも含めて)バグがFirefox本体側で修正されるよりも前にパッチを適用することができます。そのバグが本体側で修正された後は、書き換え対象のコードが見つからなくなるので、eval()
によるパッチ適用は望ましい形で失敗する(※訳註:何の変化ももたらさず悪影響も及ぼさないということ)でしょう。
あなたが書いたわけではない関数の中身に対して変更を行う事は、一般的に言ってよいアイデアではありません。
もちろんです、それは組み込みのどんな機能の挙動を変えることについても常に言えることです。しかし拡張機能の作者にとっては、やりたいことを実現するための組み込みのインターフェースや関数が存在しない時に、それを実現するための唯一の手段がこれであるという場合もあります。
Simonが既に指摘していることの繰り返しになりますが、関数へパッチを当てるのではなく関数を丸ごと置き換えるという(※訳註:ここで推奨されている)やり方は、私が述べたようなことをやろうとしているすべての拡張機能に対してそれを完全に妨げてしまいます。ですからどうか、あなたが代替になる方法を提案できないのなら、このような手法を推奨しないでください。
あなたがどんな風にそれをやったとしても、必ず酷い結果になるでしょう。
(以下翻訳中)
今まで私がみてきた限りでは、ほとんどの最小の変更がそれを壊しうるという事実にもかかわらず、(APIの変更を含む)他の種類の変更によってコードが動かなくなるケースの方が、eval()
によるパッチの適用が原因で動かなくなるケースよりも多く見られました。
gBrowser.foo
とwindow.doSomething
の両方を拡張する必要があります。gBrowser.foo
の開始時にフラグ変数をfalse
にセットして、window.doSomething
がそれをtrue
にする、doSomethingAfterFoo
はそのフラグ変数がtrue
になった時にだけ呼ばれる、という具合です。gBrowser.foo
がwindow.doSomething
を呼んだ時にdoSomethingBeforeFoo
を呼ぶ、という事でしょう(window.doSomething
が他の場所から呼ばれた場合を除いて)。このような場合、やはり両方の関数を拡張するべきです。gBrowser.foo
の開始時にフラグ変数をtrue
にセットして、終了時にそれをfalse
にセットする。window.doSomething
の開始時に、そのフラグ変数の値がtrue
であるならdoSomethingBeforeFoo
を実行する。という具合です。gBrowser.foo
を拡張し、doSomethingAnnoying
についてもそのフラグ変数がtrue
な時は何もしないように拡張することで、可能でしょう。ただし、あなたが本当にそれをする必要があって、それが何をどのように壊すのかについては、改めてよく考えてください。gBrowser.foo
を拡張し、doSomething
についてもそのフラグ変数がtrue
で且つarguments[0]
が"red"
である場合にarguments[0]
を"green"
に置き換えるように拡張すれば、それも可能でしょう。ただしその場合も、やる前にその影響をよく考えてください。maxVersionの仕組みについては、大抵の拡張機能は「バージョン3.0.*に対して互換性がある」という風に指定されているので、セキュリティの修正やマイナーリリースでの変更をキャッチアップしてくれません。
その一方で、拡張機能の作者は
――このような方法でバグにパッチを当てるのは、実に宜しくないアイデアです。Bugzillaにバグを報告して、ちゃんとしたパッチを書いてください。それが確かに明らかにバグで、安全に修正できるのであれば、その修正はマイナーリリースに取り込まれるでしょう。もしあなたが間違ったことをしていれば、フィードバックを得られるでしょう――それは、誰にも何も尋ねずに「パッチ適用」をしてブラウザの機能を壊してしまうよりも良いことです。eval()
によって、(セキュリティに関するものも含めて)バグがFirefox本体側で修正されるよりも前にパッチを適用することができます。
doSomething
(あるいは他の関数)は、 Components.classes[""].createInstance().doSomething();
という風な(※訳註:置き換え不能なネイティブのメソッドを呼ぶ)コードかもしれませんし、関数ではなくべた書きされたコードのブロックかもしれませんし、あるいは、その処理がとても頻繁に呼ばれる機能である場合は置き換え後の関数によって度し難いほどの余計な処理時間がかかるかもしれません(特に、複数の拡張機能がそういうことをするのなら)。
maxVersionの仕組みについては、大抵の拡張機能は「バージョン3.0.*に対して互換性がある」という風に指定されているので、セキュリティの修正やマイナーリリースでの変更をキャッチアップしてくれません。
これは「必ず酷い結果になる」という未来予測への反論ですよ。あなたが言っているのは実際には拡張機能の作者の過ちです。また、私に言わせれば、既存のコードを動かなくしてしまうような変更がMozillaのセキュリティアップデートで行われることがあまりに多すぎます(私が知っている最近の例はBug 442333(※訳註:eval()
の第2引数の廃止を決定したバグ)です)。
このような方法でバグにパッチを当てるのは、実に宜しくないアイデアです。Bugzillaにバグを報告して、ちゃんとしたパッチを書いてください。
既にBugzillaに報告されていたり、次のメジャーリリースに修正が取り込まれることが確定していたりするなら、同じコードを使えるでしょう。そのバグが外部の(※訳註:拡張機能などの)コードに対してのみ影響を与えるものである場合は特に、(パッチが提出されていても)そのバグが実際に修正されるまでには何ヶ月もかかることがあります。
それが確かに明らかにバグで、安全に修正できるのであれば、その修正はマイナーリリースに取り込まれるでしょう。
その修正が次のマイナーリリースに取り込まれたと仮定しても、それには最低でも1ヶ月はかかります。その修正内容をバックポートすることはそれほど害を及ぼさないでしょう。
~誰にも何も尋ねずに「パッチ適用」をしてブラウザの機能を壊してしまうよりも良いことです。
もしその拡張機能の意図するところが、そのシチュエーションにおいて行われる通常の挙動を変更する事なのであれば、それは初期状態の挙動を壊してしまおうとするもののように見えるでしょう。
ちなみに、異なる実行コンテキストでコードを動的に実行することは、eval()
のもう1つの妥当な使い方です。
eval("code to inject", context);
あるいは
domWindow.eval("code to inject");
これらは、コードを一時ファイルに書き出して、サブスクリプトローダーで読み込ませ、一時ファイルを削除する、という風なやり方によってのみ置き換えることができます。なぜなら、サブスクリプトローダーは依然としてdata: URIの読み込みを許可していないからです。(この回避策は実に面倒です。テンポラリファイルを使うことは他の問題を引き起こしかねませんから。)
「異なる実行コンテキスト」というのは、「特権がないコンテキスト」という意味ですか? それは別物です、そのコードはChrome権限を得ることはないでしょう。
「異なる実行コンテキスト」というのは、「特権がないコンテキスト」という意味ですか?
そうとは限りません。nsIWindowMediatorによって返されたDOMWindowのコンテキストは、Chrome権限のあるオブジェクトと同じになり得ます。(サブスクリプトローダーを使って読み込ませたスクリプトや、XBLのバインディングの中に書かれたスクリプトのようにwindowオブジェクトを汚染することなく動的に使われるスクリプトにおいては。)
最初の影響を見るには、以下のコードをエラーコンソールで実行してみてください:
Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser")
.eval("location");
Firefox 3.5 + Glasser 1.1.1 + Aeroの環境向けのスタイル指定がGlasser 3.5で期待通りに表示されなくなったので、手直しした。以下のスタイル指定をuserChrome.cssに加えれば、ロケーションバーの左端が丸くなります。
#urlbar {
-moz-border-radius: 11px 0 0 11px !important;
}
#urlbar > .autocomplete-textbox-container {
-moz-border-radius: 8px 0 0 8px !important;
}
#identity-box {
-moz-border-radius: 7px 0 0 7px !important;
}
※今(2009年11月24日03:22)見たらリンク先エントリおよびWikiのページからTree Style Tabの項目が消されていた。このエントリおよびリンク先のコメントを見た人にはさっぱり意味不明でワケが分からないと思うので説明しておくと、リンク先のページにはどちらもタブ系のAll-in-One型アドオンが列挙されていて、初出時はその中にツリー型タブ(Tree Style Tab)もあったのです。
多機能オールインワンを否定して、タブをツリー表示するという事に特化した単機能のアドオンとして作ってきたつもりなのに、こういう所で俎上に載せられることが非常に心外且つショックで、でも実態からそう見られてしまう部分があるのは事実なので、悲しい思いをした。
ツリー型タブにあるツリーと関係ない機能は、開発当時にその機能だけを提供する単機能アドオンを見つけられなかったからだったり、あるいは、ツリーとの緊密な連携を考慮しないと上手く働かなさそうだったりという理由からやむなく加えた物なので、外せるものならどんどん外していきたいというのが僕の正直な考えです。
で、今日探してみたら選択範囲のリンクをタブで開く機能に特化したアドオン「Selection Links」(紹介記事)を見つけて、このアドオンによってツリー型タブの持つ機能は完全に代替できる(実際にはTSTはリファラを渡すけどこのアドオンはリファラを渡さない等の違いがあるんだけどそこは気にしない)事が分かったので、当該機能をツリー型タブの次のバージョンから廃止することにしました。
あとついでに「新しいタブを開く」ボタンの表示・非表示を制御するオプションも廃止します。これはuserChrome.cssに以下のように書けば簡単に代替できるので。
.tabs-newtab-button {
display: none !important;
}
追記。意に反する紹介のされ方をした僕がキレて当てつけのために機能の削除を決意した、という風に解釈されるぜと忠告を受けたのでここにも明記しておきますが、今回の決定はそーいう下らない目的のためでは決してないです。機能の削除は「ユーザを困らせるため」ではなく、「何のためのアドオンなのかというコンセプトをはっきりさせることによってユーザ体験を向上するため(あと、コードを減らしてメンテナンス性を高めるため)」という、前向きな理由に基づいています。
確かに事実として関係ない機能まで取り込んで肥大化してしまっている場合はありますが、今回言及した表のようにそれを気付かせてくれる存在に出会えば、肥大化した部分はちゃんと切り捨てて本来あるべき姿に戻したい、と思っています。これは昨日今日で決めたようなことではなく、ツリー型タブ(およびその他の「TBE代替」のためのアドオン)を開発した当初からの方針です。
その辺の話に触れた過去のエントリがいくつかありますので、併せてご覧頂ければ幸いです。
今まで「IMAPとはサーバ上だけにメールを置いておく物なのだ」と思い込んでたので、POPから移行するのに少し抵抗があったんだけども、そうじゃなくてIMAPはPOPを包含するものでありPOP的な運用もできるのだと知って、それならいっちょ本気出して移行してみるかなと思って移行してみた。
会社マシンのThunderbirdでメールに付けたタグをどうにかして自宅のマシンからも利用できるようにしたい、という事を前々から思っていた。というのも、自宅ではプライベートのアドレスに来たメールをGmail経由でBecky! 2で受信(SPAMフィルタ代わり)して仕事メールは基本的に閲覧のみ、会社ではプライベートのアドレスに来たメールを仕事用アドレス宛に転送してThunderbirdで利用してたんだけれども、Thunderbirdを使い始めてみるとタグが便利すぎて(Tag Dialog万歳)、アドオンのバグ報告を受信したらとりあえず「未処理」「bug」のタグを付けて、処理したら「未処理」タグだけ外す、というやり方でバグを管理するような運用が染み着いてしまいまして。気がついたら、自宅のBecky! 2ではどのメールが未処理なのかさっぱり分からなくなってしまったんですわ。
まあタグで運用しなくてもフォルダ分けでもいいかなって思うんだけど、それでもとにかく家と出先とで同じフォルダ構成でメールが見えてくれないと困るわけで。じゃあIMAPだよねってなるけど、でもオフラインでメールが見れないのはイザって時に困るわけで。
やりたいのは、家PC・会社PC・Gmailの3箇所で同じデータを同期させるという運用。それぞれにメールの本文が全部ちゃんとあって、1箇所でメールを消したらそれが他の2箇所でもちゃんと反映されて、という感じにしたかった。一般的なPOPの使い方のように「受信したらサーバからはすぐ消す」というのはするつもりはない(POPの時も2箇所で受信させてたし)。
まずはアカウントウィザードでGmailのIMAP用アカウントを作成する。
アカウントができたらGmail用に設定を変更しておく。
mail.server.server*.trash_folder_name
を追加し、値を[Gmail]/ゴミ箱
に設定する。新規にGmail IMAP接続用のアカウントを作るのなら、Gmail IMAP Account Setupを使った方が楽。これをインストールすると、アカウント作成のウィザードにGmailのIMAP接続用の項目が加わる。すでに手作業でアカウントを作ってしまった後では役に立たないので、早とちりして自分でアカウントを作ってしまった僕のような人はご愁傷様です。
あと、IMAPでの運用に限らないけど、Gmailっぽいスレッドフロート式の使い方をするためには以下の設定もしておくといい。
mailnews.thread_pane_column_unthreads
をfalse
にする。この変更を行っておかないと、スレッド表示とソートを併用できない。Gmailでまともにメールの整理をしてなかった人は、これを機に整理するようにしてみましょう(僕のことです)。
というあたりを踏まえてラベルをいっぱい作っておく。
mail.check_all_imap_folders_for_new
を探して値をtrue
にする。こうすると、フォルダごとのプロパティでそのフォルダを新着チェック対象に設定する手間が省ける。extensions.checkCompatibility
をfalse
にしておかないとインストールできないので注意が必要。この3つを組み合わせることで、
という運用が可能になる。
Firefox Developers Conference 2009、企業でアドオンを開発してる所でただの宣伝ではない技術的な話が聞けそうなセッションということで、A3 「ビッツにおける拡張機能開発 (Wired-Marker 他)」見たいなあと思ってたんだけど、15:40からのトークセッションの打ち合わせがあったので見れなかったんですよね。Webに上がってる感想を見ると、AMOで公開した後のやりとりについて色々話してたそうで、うわー分かる分かる!見たかったなあ!と、悔しい思いをしてたんです。
今日になってチラシを改めて見ながら、僕の代わりにセッションを聞いてもらってた小野寺さんに「どんな発表だったん? 技術的に面白そうな所ありました?」と聞いたら、「ページ内の選択箇所の位置情報をXPathでURLに付与するとかなんとか」って言われて、「へぇーずいぶん前に僕も同じようなことをやったなあ、同じ事考える人がいるんだなあ。ひょっとして僕のコードコピペして使ったんかな?w 使われてたらちょっと嬉しいなあ。」と思って、でも公式サイト見てみても特に僕の名前とかLine Markerへの言及は無いし、「予想外したかなあ。でももし仮に僕のコードが入ってたらライセンス違反(笑)だよなあ。」と思いながらAMOの配布ページからXPIをダウンロードして中を見てみて、該当する箇所を見てみたんですよね。
わあ。懐かしいコードに出会ってしまった。
クリエイティブコモンズの表示-非営利-改変禁止ライセンス(ちなみにこのライセンスは、無料で見れる映像作品などに設定する場合に使われる「作者を明らかにしてタダで再配布しなさいよ、自分の物だと言い張ったりお金取ったりしたらあかんで、あと改造したり切り貼り素材に使ったりするのもあかんで」という性質のライセンスです)なので、具体的にコードの一部を引用して比較することは避けておきますが、content/hyperanchor/hyperanchor.jsの中で定義されてるgetXPathForNodeというメソッドと、Line MarkerのcontextMenuOverlay.jsの同名のメソッドを見比べると、変数の書き方とかループのラベルとか// fallback, for markers in another marker or other cases...
というコメントとかを見る限り、Line Marker由来のコードと見て間違いないみたいなんですよね。どうも。
amachangとAzaと後藤さんと通訳の人と打ち合わせしてる隣で堂々とCopyright(c) BITS Co., Ltd. and Prof. Okubo. All Rights Reserved.
なこのコードの解説が行われていたかと思うと、なんか笑えます。
まあそんなに独創性のあるアイデアでもないし、僕もコピペした後元のコードの作者の名前を書き忘れることがあるし、こういうのを作れる技術力のある人ならこの関数をスクラッチで書き直す(&改良する)のは大して苦じゃないでしょうから、このエントリに気付いて今後のバージョンでライセンス違反を解消してくれればいいねえと期待しつつ生暖かい目で見守りたいと思います。
で、皮肉るだけじゃあまりに非生産的なので、僕のコードに限らずMPL1.1/GPL2/LGPL2.1のTri-licenseなコード(Firefoxそのものもそうです)をこういう風に流用したい方へのアドバイスを書いておきます。トリプルライセンスなコードはトリプルライセンスまたはいずれか1つのライセンスを引き継げば問題無いので、
のような感じにしておくと、僕のようなうるさい奴が「ライセンス違反じゃー!! 天狗のしわざじゃー!!!」とどや顔で騒ぎ立てることが無くて良いと思いました。逆に言うと、そのようにしておけば誰に文句を言われることもなくFirefox内部のコードでもなんでもパクれるもとい再利用できるので、みんなどんどんFirefoxの中からコードをコピペすると良いと思います。
イベントの感想エントリより前にこんなの書いてる僕って何なんでしょうね。
追記。コメント欄にも書き込まれていますが、先方からメールでもご連絡をいただきました。ライセンス上の問題が解消されるまで一時的に、当該アドオンの公開を停止されるとのことです。当該アドオン自体の有用性を否定するつもりはありませんので、早く再公開されればいいなあと、アドオン開発者の一人として思っています。
なんでこういう嫌らしいエントリを書いて晒し上げにするわけ? メールで連絡してなしのつぶてだったら初めて晒すのでも十分なんじゃないのか? 的な批判を裸電球さんからいただきましたので改めてよくよく考えてみましたが、今回の自分の「目的」というのはどうも、「ライセンス違反を解消してもらうこと」ではなく「尊敬されなかった事への不快感の表明・発露」だったのでしょうね。
「オープンソース」という言葉が生まれたのはGNUの思想と無関係にGPLなどのライセンスの恩恵を受けるためだった、という歴史がありますが、そういう意味では僕自身も、「自分の名前をどこかに残すため」「自分の痕跡を残すため」「称賛を得るため」「遺伝子を残せないなら、代わりにコードと名前を残す」という風な大目的のためにオープンソースにフリーライドしているという部分が大きいのだと思います。だからこそ、「フリーソフトウェアの思想のため」でもなければ「コードが適切に再利用されるため」でもなく、あくまで「リスペクトされなかった、自分が蔑ろにされた、成果から得られたはずの称賛を横取りされた(と、事実はともかく自分は感じて)、それによって傷付いた自分がここにいるのだということを世界に対し表明し、同情を買い、自分の心を慰めるため」という目的を意識した行動を真っ先に取ったのでしょう。即ち、twitterやここのような「自分のフィールド」で、間接的な嫌らしい書き方で、相手を非難する、という行動を。
だからおそらく、メールで知らせて人知れず修正されたとしても、あるいは淡々と事実のみを記しても、僕の「目的」は果たされることはなかった。また、その瞬間の僕の頭には「ああ可哀想な僕たん。こんなに可哀想なんだから同情されるよね間違いないよね。」というシナリオができあがっていたから、そのシナリオに沿った行動以外を取る気にはなれなかった。
しかし一通り吐き出してやっと、おそらく大方の第三者が冷静に考えて思うであろう「馬鹿だなあ、そんな書き方をしたら余計に嫌われるだけだろ」という結論に自分自身も辿り着いたため、何ともばつの悪い、しかし今更これを引っ込める事もできない、後味の悪さと、自分という人間の小者っぷりを最大限にアピールしてしまった事への後悔を感じたわけです。今となっては、自分に対しても本件に関わった誰に対しても、残念な思いしか感じられません。
同ネタ既出の予感だけど。
amachangのHTML5のクションアウトライン解析ライブラリを使わせてもらって、HTML5ベースのセクションアウトラインに基づくページ内目次を生成し、ページの右上に貼り付けるJetpack featureを作ってみた。先日のCSS Niteの時にブツクサ言ってたのを形にした。
amachangのライブラリのライセンスが不明だったので、このJetpack featureもライセンスは明示してません。あしからず。ライブラリのライセンスはパブリックドメインとのことなので、これもパブリックドメインに……と言いたいところですが日本ではPDは存在しないので、とりあえず皆さん安心して使えるよう僕はMITライセンスが好きなのでMITライセンスとしておきます。
所感。
rgba()
とか-moz-box-shadow
とか使いまくってますよ。追記。リクエストがあったので、スライドバーへの表示に対応してみた。また、ステータスバー上のチェックボックスで、ページ内のタブの表示・非表示を切り替えられるようにした。チェックボックスの状態はFirefoxを終了した後も保持される(storageのAPIの使い方がやっと分かったので使ってみた)。
今度はJetpack用のスクラッチで。ステータスバー上に「左のタブを閉じる」「右のタブを閉じる」「他のタブを閉じる」ボタンを追加します。そんだけ。
この程度の物ならサクッと作れる。とても楽です。
追記。タブの順番を並べ替えていると期待通りに動かないバグがあったので修正した。
テキストリンクに続いてClipboard ObserverもJetpackに移植してみた。
テキストリンク同様に、普通のアドオンのClipboard ObserverのコードをJetpackで動くように手直しして、最後にJetpack用のコードを付け加えただけ……という感じ。今回は、テキストリンクでは使わなかったステータスバーへの項目追加のAPIを使ってる。
以下、詰まった点。
jetpack.storage
の使い方が分からない、というか、Web上に書かれてる情報が古くて役に立たない。APIリファレンスに書いてある書き方を試したら「この方法は古いです、もうすぐサポートしなくなります」なんてメッセージが出る。
とにかくドキュメント不足が深刻です。
追記。アウトラインリスト生成のやつでstorageの使い方がやっと分かったので、チェック状態を保存するようにした。
いわゆる軽量アドオンであるところのJetpackについて今度の日曜のFirefox Developers ConferenceでAza氏とトークショーじゃなかったトークセッションを行うにあたって、「全くJetpackを触らないまま行くのはさすがに失礼すぎるだろ常識的に考えて」と思ったので、テキストリンクをJetpack featureとして移植してみた。
以下、詰まった点。
==
で比較すると、ラップされてるノードが同じノードでもfalse
になる。===
や!==
で比較すればちゃんと意図通りに判定される。これは盲点だった。(普通にアドオンを作る時だと==
で期待通りに判定されるので)やっつけ移植なので、URLっぽい文字列をダブルクリックしたら新しいタブを開く、という機能しかないです。そのくせ25KBもありやがる。
スクリプトの中身はぶっちゃけ普通のテキストリンクのコードのコピペです。というか、Jetpackで動かないであろう部分を削っていって残ったのがコレなんで。最後の方にちょこっとだけ、JetpackのAPIを使ってページの読み込み完了を監視する処理が入ってて、そこが本題です。
いいな、と思った点。