Displaying posts tagged with

“Greasemonkey”

XPCNativeWrapperとunsafeWindowの間で同期処理を行う

先日使い方を学んだ MessageEvent と、location.href+javascript:ハックとJSDeferred userscript版とを組み合わせ、XPCNativeWrapper と unsafeWindow の間で同期処理を行うコードを書いてみた。

まずは MessageEvent を使って前回 evalInPage 相当の処理を作る。キャンセル処理も入れたかったので UnsafeWrapper というオブジェクトにまとめることにした。前回より長くなったように見えるが、実際には MessageEvent の lastEventId が使えるようになった分、簡潔になっている。

var UnsafeWrapper = new (function() {
	var seqId = 0, waiting = {}, self = this, noop = function(){};
 
	function dispatch(data, id) {
	    var e = document.createEvent("MessageEvent");
	    e.initMessageEvent('GM_UnsafeWrapper_returned', true, false,
			       data, location.protocol + "//" + location.host,
			       id, window);
	    document.dispatchEvent(e);
	}
 
	function listen(func) {
	    document.addEventListener('GM_UnsafeWrapper_returned', func, false);
	}
 
	listen(function(e) {
		if(waiting[e.lastEventId]) {
		    waiting[e.lastEventId].call(self, JSON.parse(e.data));
		    delete waiting[e.lastEventId];
		}
	    });
 
	this.exec = function(func, args, callback) {
	    var userFunc = "("+ func +").apply(null,"+ JSON.stringify(args || []) +")";
	    waiting[seqId] = callback || noop;
	    location.href = "javascript:void "+
	    (function(dispatch, ret, id) { dispatch(JSON.stringify(ret), id) }) +
	    "("+ dispatch +","+ userFunc +"," + JSON.stringify(seqId) +")";
	    return seqId++;
	};
 
	this.cancel = function(id) {
	    delete waiting[id];
	};
    });

そして Deferred に対するアダプタを書く。とても簡単。

with(D()) {
    function unsafeExec(func, args) {
	var d = new Deferred();
	var id = UnsafeWrapper.exec(func, args, function(ret) {
		d.call(ret);
	    });
	d.canceller = function(){ UnsafeWrapper.cancel(id) };
	return d;
    };
}

これで例えば、GM_xmlhttpRequest で外部ドメインからデータを取得して、そのデータを整形して、unsafeWindow 側で表示処理を行って、そしてその結果をuserscript側で受け取って…という処理は次のように書くことができる。

with(D()) {
 
xhttp.get("http://example.com/")
    .next(function(res) {
            var data = res.responseText;
            //....
 
	    return unsafeExec(function(data) {
                    $.each(data, function(k,v) {
                      //...
                    });
                    //...
		    return result;
		},
		[data]);
	})
    .next(function(result) {
          //...
	});
 
}

より実用性を高めるのであれば、location.href で実行するコード全体を try {} で囲み、catch したエラーをJSONに変換して _failed イベントを dispatch して、Deferred の fail に転送すれば、unsafeWindow 側で起こったエラーをuserscript側のDeferred chainで捕捉できるようになる。

参考:

XPCNativeWrapperとunsafeWindowの間でデータを送受信する

XPCNativeWrapperの外側でスクリプトを評価する関数。Firebugも同じことをしている。unsafeWindowを触らないので安全。

function evalInPage(fun) {
  location.href = "javascript:void (" + fun + ")()";
}

SmartLDR更新 – 素人がプログラミングを勉強するブログ

これは面白い!Function.prototype.toString が関数のソースコードを返すことを利用した、巧妙にして簡潔なハック。

次のように引数を渡せるように改良すればさらに強力になる(引数に渡せるのはJSON化可能な値のみ)。

function evalInPage(func, args) {
    var argStr = JSON.stringify(args || []);
    location.href = "javascript:void "+ func +".apply(null,"+ argStr +")";
}

GM_xmlhttpRequest を使って別ドメインから得たデータを、unsafeWindow 上のライブラリを使って表示する、といったコードが安全かつ自然に書けるようになる。

// データの取得は Greasemonkey で行う
GM_xmlhttpRequest({ method:"GET",
                    url: "http://example.com/api",
                    onload: function(response) {
                      var data = response.responseText;
                      //...
                      evalInPage(render, [data]);
                    }
                  });
 
// データの表示は unsafeWindow 上で行う
// jQueryなど unsafeWindow 上のライブラリが使える
function render(data) {
  $.each(data, function(k,v){
    //...
  })
}

これだけでも十分便利なのだが、「返り値が利用できない」「Firefoxのlocation.hrefは非同期で実行される」といった難点があるらしい。それを回避するためにunsafeExec on JSDeferred – 枕を欹てて聴くにおいてはJSDeferredというライブラリを使う方法が紹介されているのだが、コードを見る限り unsafeWindow に対するアクセスが必要になるようだ。

unsafeWindow にアクセスせずにGreasemonkeyに制御を戻すには、DOM Eventを使うのが良いと思う。以下、DOMNodeInserted イベントを使って実装してみた(返り値として利用できるのはJSON化可能な値のみ)。

追記: Constellationさんからコメントをいただきました。DOMNodeInsertedなどのイベントを使わなくても、直接createEvent/dispatchEventでイベントを生成・データを送受信する方法があるようです。 詳細はこのページの Constellationさんのコメントや、『Greasemonkey スクリプトとイベントで通信: Days on the Moon』など参照してください。

function getUniqId() {
    do {
        var id = "_tmp" + String(Math.random()).slice(2);
    } while(document.getElementById(id));
    return id;
}
 
function evalInPage(func, args, callback) {
    var argStr = JSON.stringify(args || []),
        userFunc = "("+ func +").apply(null,"+ argStr +")";
 
    if(!callback) {
        location.href = "javascript:void "+ userFunc;
        return;
    }
 
    var div = document.createElement('div'),
        id = getUniqId();
    div.id = id;
    div.style.display = 'none';
    document.body.appendChild(div);
    div.addEventListener('DOMNodeInserted', function(e){
            callback(JSON.parse(e.target.nodeValue));
            div.parentNode.removeChild(div);
        }, false);
 
    location.href = "javascript:void "+ (function(ret, id) {
            document.getElementById(id)
            .appendChild(document.createTextNode(JSON.stringify(ret)));
        }).toString() + ".call(null,"+ userFunc +"," + JSON.stringify(id) +")";
}

JSDreferredを使う方法と同じく、戻り値を受け取るコールバック関数を指定する。

evalInPage(function(obj, num) {
        //この中のコードは unsafeWindow で実行される
        return { answer: obj.a + num };
    },
    [{a:99}, 1],
    function(ret) {
        //この中のコードは Greasemonkey で実行される
	alret(ret.answer);
    });

多分まじめに作り込めば一種のRPCシステムみたいになると思う。

ow.ly や oneclip.jp のフレームを外す user script

ow.lyoneclip.jpなど、元コンテンツをiframe内に表示するタイプのURL短縮サービスから、自動的に元URLにリダイレクトするuser scriptを作りました。

Redirect to the iframed URL

同様の機能を持つものとしてiframe redirectorがあり、今まで私も使用していたのですが、対象サービスのURLやHTML構造に依存するコードが含まれているため、対象サービスの構成が変化すると動かなくなってしまいます(少し前に am6.jp の仕様が変わって動かなくなりました。現在は修正されています)。

そのため拙作のスクリプトではURLやHTMLに依存するコードは使わず、「面積比でbodyの4分の1を占めるiframe」を元コンテンツと判断するようにしました。おそらくほとんどのケースで問題なく動作するものと思われます。

デフォルトでは以下のURLで動作します。

  • http://ow.ly/*
  • http://oneclip.jp/*
  • http://am6.jp/*

今後新たに同種のサイトが登場した場合は(登場してほしくないのですが)、Greasemonkeyの管理ダイアログから「ユーザスクリプトを実行するページ」にURLを追加してください。スクリプトの修正は不要です。

Firefox 3.5.7 + Greasemonkey 0.8.2 および Mac版 Google Chrome 4.0.295.9 で動作確認しています。

参考

URL短縮サービス ow.ly は実URLが表示されずに危険 → グリモン書いた – まちゅダイアリー(2009-11-05)