2012年11月17日土曜日

Greasemonkeyで広告を消去できないケースの再現例

前の投稿で書いた「Greasemonkeyで広告を消去できないケース」を再現する例です。 広告消去のアドオンを作りたい人にとっては、簡易サーバとかログの確認とかがテストの参考になるかもしれません。

次の環境で確認。

  • Windows 7 home 64bit
  • Firefox 16.0.2
  • Greasemonkey 1.4
  • jdk 1.7

まずはGreasemonkeyでimgタグを消すコードを書きます。 このコードはlocalhostでのみ有効です。

// ==UserScript==
// @name        remove image test simple
// @namespace   http://localhost/kK1r01AhGYl0m52r01AhGYlA
// @description remove image test simple
// @include     http://localhost/*
// @version     1
// @grant       none
// ==/UserScript==

(function(){

    console.log('Greasemonkey test.');

    var imgList = document.getElementsByTagName('img');
    for (var i = imgList.length - 1; 0 <= i ; i--) {
        var imgElem = imgList[i];
        console.log('remove img -> ' + imgElem.id);
        var parent = imgElem.parentNode;
        parent.removeChild(imgElem);
    }

})();

localhostの簡単なhttpサーバをjavaで書きます。

// HttpServerIdler.java
package httpserveridler;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.*;
import java.net.*;

public class HttpServerIdler implements HttpHandler
{
    public final static int SERVER_RESPONSE_OK = 200;
    public final static int SERVER_PORT = 80;
    public final static int SERVER_STOP_DELAY = 10;
    public final static int IMAGE_NUM = 100;

    private HttpServer _server = null;
    private Thread _serverThread = null;

    private byte[] _responseBody;

    public static void main(String[] args)
    {
        HttpServerIdler thisApp = new HttpServerIdler();
        thisApp.run();
    }

    private void run()
    {
        initResponseBody();
        startServer();
        //WaitEnterKey("Push enter to stop.");
        //stopServer();
    }

    private void initResponseBody()
    {
        String text = "<html><body>\n";
        for(int i = 0; i < IMAGE_NUM; i++)
            text += "<div><img src=\"https://www.google.co.jp/images/srpr/logo3w.png\" id=\"" + i + "\"/></div>\n";
        text += "</body></html>";

        _responseBody = text.getBytes();
    }

    private void WaitEnterKey(String message)
    {
        System.out.print(message + " > ");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try
        {
            br.readLine();
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
    }

    private void startServer()
    {
        System.out.println("Confirm a command : server start");
        _serverThread = new Thread()
        {
            @Override
            public void run()
            {
                try
                {
                    _server = HttpServer.create(new InetSocketAddress(SERVER_PORT), 0);
                    _server.createContext("/", HttpServerIdler.this);
                    _server.start();
                }
                catch (IOException exc)
                {
                    exc.printStackTrace();
                }
            }
        };
        _serverThread.start();
        System.out.println("Server is started.");
    }

    private void stopServer()
    {
        System.out.println("Confirm a command : server stop");
        System.out.format("Wait %d seconds...\n", SERVER_STOP_DELAY);
        assert _server != null && _serverThread != null;
        _server.stop(SERVER_STOP_DELAY);
        System.out.println("Server is stopped.");
    }

    @Override public void handle(HttpExchange ex)  throws IOException
    {
        OutputStream os = ex.getResponseBody();
        try
        {
            ex.getResponseHeaders().add("Content-Type", "text/html");
            ex.sendResponseHeaders(SERVER_RESPONSE_OK, _responseBody.length);
            writeResponseBody(os);
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
        finally
        {
            try
            {
                os.close();
            }catch(Exception exc){}
        }
    }

    private void writeResponseBody(OutputStream os) throws Exception
    {
        int half = _responseBody.length / 2;

        os.write(_responseBody, 0, half);
        WaitEnterKey("Push enter to continue.");
        os.write(_responseBody, half, _responseBody.length - half);
    }
}

1年前のコードを適当に再利用した手抜きコードです。 デバッガ上で動かし、デバッガの停止ボタンで止めることを想定しているのでサーバを止めるコードはコメントアウトしてます。

これを起動してブラウザから http://localhost/ にアクセスすると次のようなhtmlが返されます。 web上の適当な画像(サンプルではgoogleのロゴ)へのリンクが100個並んだだけのhtmlです。

<html><body>
<div><img src="web上の適当な画像" id="0"/></div>
<div><img src="web上の適当な画像" id="1"/></div>
<div><img src="web上の適当な画像" id="2"/></div>
...
<div><img src="web上の適当な画像" id="99"/></div>
</body></html>

ただし、writeResponseBodyメソッドに書いてあるとおり、半分送信したところで一時停止してキー入力待ちになります。

Greasemonkeyを有効にしたfirefoxでこの簡易サーバにアクセスしたときのログを見てみましょう。 メニューからツール/web開発/webコンソールを選べば、ブラウザの下の方にログが表示されます。 サーバが一時停止中のログの様子は、

GET http://localhost/ [HTTP/1.1 200 OK]
GET https://www.google.co.jp/images/srpr/logo3w.png [HTTP/1.1 200 OK 111ms]

まずlocalhostにアクセスしてGET。 「HTTP/1.1 200 OK」が表示されます。 htmlが読み込み終わったのならOKの右にダウンロードの所要時間が表示されるのですが、送信が止められているので表示はありません。

次の行はgoogleのロゴのGETです。 こちらは当然localhostとは関係なくダウンロードされます。 111msで読み終わっているのが確認できます。

htmlの受信(とパース)が終わっていないのでGreasemonkeyのコードはまだ動いていません。 そして、前の投稿で書いたとおり、受信済みのhtmlや画像を元に仮の不完全なページが表示されます。 ブラウザのページ領域にはgoogleのロゴが表示されます。 もしこれが広告抑止のユーザスクリプトだったなら「正常に動作できなかった」という事になります。

ちなみに、firefoxではダウンロードしたhtmlのソースとパース済みのソースの両方を確認することができます。

  • ダウンロードしたhtmlのソース : 右クリックメニューの「ページのソースを表示」
  • パース済みのソース : ページの一部(または全部)を選択して、右クリックメニューの「選択した部分のソースを表示」

ただし、ページの読み込みが不完全な状態で「ページのソースを表示」をしても何も表示されません。 つまり、今説明中のこの状態では何も表示されません。 「選択した部分のソースを表示」は表示可能です。 全選択をして「選択した部分のソースを表示」をすると、送信が途切れて閉じていないはずのhtmlタグ、bodyタグが勝手に閉じられているのが確認できます。

簡易サーバでエンターキーを押して残りのhtmlを送信するとログは次のようになります。

GET http://localhost/ [HTTP/1.1 200 OK 24724ms]
GET https://www.google.co.jp/images/srpr/logo3w.png [HTTP/1.1 200 OK 111ms]
HTML ドキュメントの文字エンコーディングが宣言されていません。ドキュメントに US ASCII 外の文字が含まれている場合、ブラウザの設定によっては文字化けすることがあります。ページの文字エンコーディングはドキュメント中または転送プロトコルで宣言されなければなりません。 @ http://localhost/
Greasemonkey test.
remove img -> 99
remove img -> 98
remove img -> 97
...
remove img -> 1
remove img -> 0

GET http://localhost/ に取得までの時間が追加されています。 そして、「Greasemonkey test.」のログ以降、ユーザスクリプトの動作が確認できます。 ブラウザのページ領域のgoogleのロゴは消去されます。

ついでに、「ページのソースを表示」をすると受信したhtmlが表示できます。 全選択をして「選択した部分のソースを表示」をすると、imgタグが消えて外側のdivタグだけが残っているのが確認できます。