dataスキームの画像をブロックしてみる - Chrome編2012年03月22日 03時52分25秒

前のエントリの続きで、今度はGoogle Chrome行ってみよう。といっても、それほど書くことあるわけじゃないので比較的アッサリ気味の内容で。

他にもいろいろあるんだけど、Firefoxでソースをざっくり眺めて馴染みがでたので、ターゲットはAdblock Plus。現在タイトルに「(Beta)」がついてる1.2版です。

こちらもFirefox版と同じく、単純に「data:image/*」みたいな登録をしてもフィルタリングされないので、またまたデバッグコードを仕掛けて調べてみることに。

extensionのインストールされたディレクトリをざっと眺めてみたらmanifest.jsonてのがあったのでそちらを見てみる。

manifest.jsonの内容から推測するに、メインで使用されるのはどうやら「contentScript2.js」らしいのでそちらを見てみたところ、beforeloadHandlerという関数からshouldBlock()にURLを渡して判断してるっぽいのでその辺りにデバッグコードを仕掛けて調べたら、dataスキームURIを相対URLとみなして、現在のドキュメントURLからの絶対URLに変換してた。こりゃ引っかからんわ。

コードにパッチ当てちゃってもいいんだけど、モノは試しで「http://*/*data:image/*」なんてフィルタ登録してみたらあっさりうまくいっちゃった。まぁURLの途中に「:」を挟んだパターンがあったら誤爆する可能性もあるんだけど、とりあえずこれでいいか。

ただ、URLフィルタにマッチした場合でもHTTPでの通信そのものは発生しちゃうみたいで、それだったら「##img[src*="data:image"]」のほうが誤爆の恐れもないし安全かも。

あ、あとcssでbackground-imageに指定してる場合なんかはブロックされないっぽいです。

dataスキームの画像をブロックしてみる2012年03月22日 03時06分18秒

面白いネタ、いただきました。

にーまるどっとこむ(旧「突然消失するかもしれないブログ」)のなかのきえたさんから、こんなメンションが飛んできた。

Adbloc PlusではData URIはフィルター対象に指定出来ないのですが、何か技術的に難しい理由があるのでしょうか?

Adblock Plusはこれまで使ったことなかったんだけど、なんだかえらく興味をひかれたので調べてみることに。

よくわからんが使ってしてみると

ちょっと調べたところ、Firefox用とChrome用があるみたいなんだけど、とりあえずFirefoxのほうを試してみる。

まずはAdblock Plusをインストールし、適当にググってみたページなんかを参考にして見よう見まねで

data:image/*
みたいな感じで登録、手持ちであったdataスキームで画像やらスクリプトを食わせているページを開いてみたが、なんの変化も見られない。ふむ。

ドキュメント類もろくすっぽ読まずに機能拡張のソースコードを追っていき、nsIContentPolicyというインターフェイスの実装を提供しているということがわかったので、ContentPolicy.jsmというモジュールのあちこちにデバッグコードをひっかけて調べたところ、内部で持っているスキームのホワイトリストにマッチするので、URLの突き合わせが行われる前にブロックされないことが確定していると判明。さて、ホワイトリストはどこに持ってるんだ?

ググってみると、あるもんだ。

キーワードは失念したがググってみたところ、adblockplus.orgのドキュメントが見つかった。

これ見ると、どうやら「extensions.adblockplus.whitelistschemes」で値を持ってるみたい。

さっそくabout:config開いて「adblockplus.whitelist」くらいでフィルタかけてみたらこんな感じで設定項目が見つかった。

なるほど、先のドキュメント通りに許可スキームをスペース区切りで持ってるので、"data"を取り除いてみることに。

一旦ブラウザを再起動して再度dataスキームを使ったページにアクセスしてみたら、こんどはちゃんと画像もスクリプトもロードされない!おお、やった。

実はもっと簡単な方法があったのねん…

なんてとこまで調べたあたりで、なかのきえたさんからこんなツイートが。

これで消せたみたいですけど、これであってます?→「##img[src*="data:image"]」
よくよく(よくよく、じゃなくても)調べてみたら、ブロックリストの書き方を解説してるページで詳しく解説されてるじゃない!

これはURLフィルタではなく、「非表示要素フィルタ」というものらしく、要素の属性に対してのマッチングパターンを記述できると。なんだ、こっちのほうが圧倒的に簡単じゃん… orz

ただ、負け惜しみじゃないんだけど、この書き方だと

  • 画像の場合はimg要素で読み込んでいるものだけが対象、cssでbackground-imageなんかに指定してる場合はブロックできない
  • script要素には(表示もクソもないので)効果がない
という違いはありますが。えぇ、負け惜しみじゃないですよ?

ということで、サンプルページをば

作ってみました。

dataスキーム使ったサンプルページ

img要素で画像表示してるところと、cssでbackground-imageにdataスキーム指定してるところと両方あるので、「data:image/*」と「img[src*="data:image"]」のフィルタの違いをご堪能あれ。

オマケ情報

URLフィルタ使う場合は、「data:*」みたいな乱暴な指定もできます。これだとcssだろうがscriptだろうがなんでもブロックできるっぽいです。

次はChorme版だ。

と思ったけど、ちょっと記事が長くなったので別エントリを起こす予定です。

追記

Chrome編、簡単に書きました。

SafariとChromeの判別があやしくなったので改善してみる2011年08月25日 20時10分51秒

夏も終わり間近というこのタイミングで今年最初のエントリになっちゃうんだけども、デスクトップ版Safariが5.1になったことで以前書いた「SafariとChromeを判別してみる」の方法が通用しなくなっちゃったのであわてて更新してみる。ほんとは中華パッド3号とか4号とかのこと書きたかったんだけども。

前回のおさらい

前回のアプローチは、SafariとChromeでwindow.constructorが違っている(SafariはObject、ChromeはDOMWindow)ことを利用してこんなアプローチをしてた。

// Safariはtrue、Chromeはfalseを返す関数
<script type="text/javascript">
window.check1 = function() {
  try {
    return window.constructor.prototype.alert == undefined;
  } catch(e) {
    // IEとかエラーでるし。
    alert(e.message || e.description);
  }
}
</script>

<button type="button" onclick="alert(check1())">check1</button>

ところが、Safari5.1からはwindow.constructorがDOMWindowConstructorとやらに変わっちゃったので、Chromeと判別されちゃう。さて、困りました。

Safari Developer Libraryを探したら

んで、急いでAppleのSafari Developer LibraryDocument Additions Referenceプロパティを確認したところ、「webkitCurrentFullScreenElement」「webkitFullScreenKeyboardInputAllowed」「webkitIsFullScreen」なんてのが見つかった。

念のためChromeのJavaScriptコンソールで「document.webkit」まで入力してタブキーで候補をさらってみたところ「Current~」とか「FullScreen~」、「Is~」とかは表示されなかったのでなんとなくで「webkitIsFullScree」を採用することに決定。

ただし、プロパティの名前からしてbooleanを返すだろうから、単純に

!document.webkitIsFullScreen

なんて試しても判別がつかないので、undefinedかどうかをテストすることにして、こんな感じに。
<script type="text/javascript">
window.check2 = function() {
  try {
    return window.constructor.prototype.alert == undefined ||
           document.webkitIsFullScreen !== undefined;
  } catch(e) {
    return e.message || e.description;
  }
}
</script>

<button type="button" onclick="alert(check2())">check2</button>

で、こうなりました。

この変更を適用すると、こんな感じに。前回同様is_webkitとis_mobileの判定が出来る前提で。

<script type="text/javascript">
// Safariかどうか
window.is_safari2 = (function() {
    // WebKitじゃなきゃ当然Safariじゃないわな
    if(! window.is_webkit) return false;
    if(window.is_mobile) {
        // モバイル版の場合、SVGをサポートしてたらSafari
        return !! window.SVGColor;
    } else {
        // デスクトップ版の場合、windowコンストラクタのプロトタイプで判別
        try {
            // * ココ修正!! *
            // どっちかtrueならSafari
            return window.constructor.prototype.alert == undefined ||
                   document.webkitIsFullScreen !== undefined;
        } catch(e) {
            // そもそもwindow.constructor とか そのプロトタイプにアクセスできない
            // ブラウザもあるので try - catch しとく
            return false;
        }
    }
})();

// Chromeかどうか
window.is_chrome2 = (function() {
    // WebKitじゃなきゃ(ry
    if(! window.is_webkit) return false;
    // 後は基本的にSafariと逆
    if(window.is_mobile) {
        return ! window.SVGColor;
    } else {
        try {
            // * ココ修正!! *
            return window.constructor.prototype.alert != undefined &&
                   document.webkitIsFullScreen === undefined;
        } catch(e) {
            return false;
        }
    }
})();

</script>

<button type="button" onclick="alert(window.is_safari2)">Safariっすか?(改)</button>
<button type="button" onclick="alert(window.is_chrome2)">Chromeっすか?(改)</button>

Chrome 13まではこれで大丈夫だけど、フルスクリーンがどうのこうのなんて機能はOS X Lion向けっぽいのでいずれこれでも判別できなくなっちゃいそうではあるが。

つか、そろそろ中華パッドネタ書いときたいなぁ。もうすでに旬をすぎちゃってるんだけども。

追記

Safari且つChromeとか判定されたり、前回の記事と同時に表示すると上書きされちゃったりなど細々と考慮漏れがあったりしたのでちくちく修正してました。もう平気かな。

追記2(2011.12.28)

ありゃ、気付いたらChromeもdocument.webkitIsFullScreenとかサポートしちゃってるしorz さて、どうするべ…

→ window.chrome が使えそう。なぜこれに気付かないし。

SafariとChromeを判別してみる2010年12月18日 04時32分45秒

※:Safari 5.1がChromeと区別できなくなったので修正版作ってみました。(11.08.25)

はい、またも3週間もほったらかしにしてたわけですが、ようやく前回の続きで、レンダリングエンジンがWebKitだった場合にAppleのSafariなのか、GoogleのChromeなのかを判別してみようかと。

当初の考えでは、まずSafariなのかChromeなのかを判断して、それからモバイル版なのかを見ようと思ってたんだけど、「SafariとモバイルSafariにはあって、ChromeとモバイルChromeには存在しないプロパティ」またはその逆ってのが見つからなかったのよね。

仕方ないのでデスクトップ版同士、モバイル版同士の差異を探して、

  • まずはモバイル版かどうか判断し
  • その後にそれぞれSafariかChromeかをチェックする
という方法に落ち着いた。

あ、この記事では便宜上、Androidの標準ブラウザを「モバイル版Chrome」とか呼んじゃってるけど、これたぶん正式な名前じゃないみたい。あくまで便宜上の呼称と思ってくださいな。

まずはモバイル版かどうか見分ける

これは前回の最後にふれてたけど、モバイル版ブラウザにはデバイスの向きを示す「window.orientation」というNumberなプロパティがあり、これはデスクトップ版には今のところ実装されてないのでこれを使ってみる。

<script type="text/javascript">
// WebKitかどうか。
window.is_webkit = (function() {
    try {
        return window.navigator.taintEnabled == undefined;
    } catch(e) {
        return false;
    }
})();

// モバイルブラウザかどうか
window.is_mobile = (function() {
    return window.is_webkit && ! isNaN(window.orientation);
})();
</script>

事前にWebKitであるかもあわせて判別しておく。 あとはこの情報を参照すればいいだけ。
<button type="button" onclick="alert(window.is_mobile)">モバイル版のWebKit?</button>

Safariであるかどうか - モバイル編

まずモバイル版の違い。エミュレータベースでしか確認してないけど、Android 2.3(Gingerbread)になってもモバイル版のChromeはまだSVGをサポートしてない模様。しかもありがたいことにWebKitはDOMとかブラウザ由来のオブジェクトのコンストラクタがちゃんとwindowのプロパティとしてアクセスできるので、SVG関連のコンストラクタの有無をチェックすればいいわけだ。

が、モバイルSafariのほうもSVGの実装が段階的に進んでいるため、バージョンによって存在するものとしないものがあるので、なるべく基本的なオブジェクトを使う必要がある。

ここではSVGColorを使うことにする。要はwindow.SVGColor が undefined かどうかを見るというわけ。コードは後述。

Safariであるかどうか - デスクトップ編

で、今度はデスクトップ版の場合だが、これがなかなか苦しかった。さすがに各ブラウザともHTML5街道へまっしぐら状態なのでなかなかブラウザ固有のものが見つからないのだ。windowのプロパティをfor..inで列挙してみると、たまに片方にしか存在しないプロパティ名が見つかるのだが、単にDontEnum属性がついてるだけでオブジェクトとしては存在してたりするし。

そんなわけで単純な存在の有無のチェックじゃ実現できないんだけど、windowプロパティの列挙時に値までチェックしてみたところちょっと面白いことに気づいた。たとえばデベロッパコンソール上でwindowのコンストラクタを出力してみると、Safariの場合は

> window.constructor
  function Object() {
      [native code]
  }

Chromeの場合は
> window.constructor
  function DOMWindow() { [native code] }

と、コンストラクタ関数がまったく異なっているようなのだ。

Safariの場合、コンストラクタがObject()ってことは、window固有のプロパティはコンストラクタから継承してるわけではないと推測できるし、ChromeのほうはモロにDOMWindowという名前なのでこれにwindow固有プロパティが実装されてそうだと推測できる。ってことで、Chromeで試してみた。

> window.constructor.prototype.alert
  function alert() { [navite code] }

お!いい感じですよ?じゃ、Safariでは?
> window.constructor.prototype.alert
  undefined

よし!予想通り!!要するに、window.constructor.prototype.alert が undefined ならSafari、そうでないならChromeと判断して間違いない、と。

これ試したのはChrome 8だったので、以前のバージョンがどういう実装になっていたのか、USBブート可能なポータブル版のChrome 4やら5やらを探し出してきて試してみたところ、まったく同じ状況だったのでこれで問題なさそう。

で、こうなる、と。

ようやく判別方法が固まったので、以下のような感じで。あ、先ほどのis_webkitとis_mobileの判断がでてることが前提でね。

<script type="text/javascript">
// Safariかどうか
window.is_safari = (function() {
    // WebKitじゃなきゃ当然Safariじゃないわな
    if(! window.is_webkit) return false;
    if(window.is_mobile) {
        // モバイル版の場合、SVGをサポートしてたらSafari
        return !! window.SVGColor;
    } else {
        // デスクトップ版の場合、windowコンストラクタのプロトタイプで判別
        try {
            // undefinedならSafari
            return window.constructor.prototype.alert == undefined;
        } catch(e) {
            // そもそもwindow.constructor とか そのプロトタイプにアクセスできない
            // ブラウザもあるので try - catch しとく
            return false;
        }
    }
})();

// Chromeかどうか
window.is_chrome = (function() {
    // WebKitじゃなきゃ(ry
    if(! window.is_webkit) return false;
    // 後は基本的にSafariと逆
    if(window.is_mobile) {
        return ! window.SVGColor;
    } else {
        try {
            return window.constructor.prototype.alert != undefined;
        } catch(e) {
            return false;
        }
    }
})();

</script>

<button type="button" onclick="alert(window.is_safari)">Safariっすか?</button>
<button type="button" onclick="alert(window.is_chrome)">Chromeっすか?</button>

そのうちライブラリにするよ。多分。

ということで、ようやく当初の目的どおりの判別ができるようになりましたとさ。

ここまでのスクリプトはすべて、window.onloadを待たずに判断できるので、単独ライブラリで実装して一番最初に読み込むスクリプトに仕立て上げちゃえばいいんだけど、ちょっとまとめるのがめんどくさいので、まぁそのうち。

追記

ちなみに、window.constructor.prototype のプロパティでの判断は、モバイル版では通用しません。モバイル版ChromeはSafariと同じで window.constructor.prototype.alert は undefined になります。と思いました。確か。

<button type="button" onclick="try { alert(window.constructor.prototype.alert); } catch(e) { alert(e); }">window.constuctor.prototype.alert は?</button>

JSでブラウザを判別してみよう2010年11月25日 05時07分32秒

油断してたらまた4ヶ月も更新してなかったのだが、なんとなくJSでブラウザ判別を行うという、わりとありがちな事に挑戦してみる。

ライブラリとしてまとめようと思ってたんだけど、そろそろ眠いので、今日のところは断片的な記述にしておこう。

何を判別する?

メジャーどころだったらたいていはレンダリングエンジン≒ブラウザが成立しそうなもんなんだけど、WebKitの場合はSafariとChromeの両方がなかなか普及してるので、「レンダリングエンジンが何であるか」と「ブラウザが何であるか」の2種類を判別することにする。で、Safari/Chromeの場合はモバイル版かどうかも見ないとね。

ということで、レンダリングエンジンは

  • Trident
  • Gecko
  • Presto
  • WebKit
、ブラウザは
  • IE
  • Firefox
  • Opera
  • Safari
  • Chrome
を判別してみる。

まずは、Presto = Opera。

めちゃくちゃ手抜きなんだけど、window.operaがundefinedじゃなければPrestoっつーことで。しかもPrestoだったら何も考えずにOperaっつーことで。

<button type="button" onclick="alert(!! window.opera)">レンダリングエンジンははPresto?</button>

<button type="button" onclick="alert(!! window.opera)">ブラウザはOpera?</button>

次はGecko = Firefox。

まぁ、GeckoをみんなFirefoxとみなすのは乱暴なのはわかってるんだけども、こちらも手抜き。

昔、MooToolsの1.2.2のブラウザ判定部分のソース見たときは、window.document.getBoxObjectForがundefinedでなければGeckoてな判断してたみたいなんだけど、Firefox 3.6の環境で試したらこれが成立しなくなってたのでwindowのプロパティを総当りで調べたところ、window.mozInnerScreenXあたりが使えそうなので組み合わせで判別してみる。

<button type="button" onclick="alert(window.document.getBoxObjectFor != undefined || window.mozInnerScreenX != undefined);">レンダリングエンジンはGecko?</button>

<button type="button" onclick="alert(window.document.getBoxObjectFor != undefined || window.mozInnerScreenX != undefined);">ブラウザはFirefox?</button>

IEかどうか、までは簡単。

IEかどうかを判別するのは、window.ActiveXObjectをチェックするのが定番なんだけども、念のためMSDNを見てみたら、なんかIE9で廃止になっちゃいそうな記述が。

びっくりしてIE9ベータで確認したらnullは返してこなかったんだけど、ちょっと怖いのでもうひとつの定番「window.document.all」を使うことに。でもこれ、Operaも実装してるんだったよな、確か。 っつーことで、こんな感じに。

<button type="button" onclick="alert(!!((window.ActiveXObject || window.document.all) && !window.opera))">レンダリングエンジンはTrident?</button>

IEのバージョンは細かく見てみたい。

で、IEの場合は他のブラウザよりもバージョンの違いをなるべく厳密に判別したくなるので、

  • IE6よりも前
  • IE6
  • IE7
  • IE8
  • IE9
をなんとか判別してみようかといろいろ探してみたところ、これまたMSDNにDetecting Internet Explorer More Effectivelyなんてぴったりの記事が見つかった。冒頭のほうではユーザエージェントで判別する方法を記載してるんだけど、これじゃ心もとないのでもう少し読み進めてみたら、「Another IE version detector snippet」と題するコメントが
Another IE version detector snippet
function getIEVersion(odoc){
if (odoc.body.style.scrollbar3dLightColor!=undefined)
{
if (odoc.body.style.opacity!=undefined) {return 'IE9';}
else if (odoc.body.style.msBlockProgression!=undefined) {return 'IE8';}
else if (odoc.body.style.msInterpolationMode!=undefined) {return 'IE7';}
else if (odoc.body.style.textOverflow!=undefined) {return 'IE6'}
else {return 'IE5.5 or lower';}
}
}
なるほど、styleプロパティでチェックするのね。ただこのサンプルどおりbody.styleをチェックする方法だとページロードが完了しないと判定できないので不便なので、head要素でチェックをしてみることに。あ、あとIE9かどうかの判別はwindow.msPerformanceでチェックしたほうがよさげ。

で、こんなコードを実行して、

<script type="text/javascript">
window.detected_ie_name = (function() {
    if(window.ActiveXObject == undefined && window.document.all == undefined) return "IEじゃない";
    if(window.msPerformance != undefined) return "IE9";
    var h = document.getElementsByTagName("head")[0];
    if(h.style.msBlockProgression != undefined) return "IE8";
    if(h.style.msInterpolationMode != undefined) return "IE7";
    if(h.style.textOverflow != undefined) return "IE6";
    return "IE5.5かそれ以前";
})();
</script>

こんな感じのボタンを貼ってみる。
<button type="button" onclick="alert(window.detected_ie_name)">IEのバージョンは?</button>

WebKitか否か。

MooTools 1.2.2では、navigator.taintEnabledのチェックでもって判別してた。これなんぞ?と思ったら、よい解説が見つかった。

navigator.taintEnabled() というのは、「ユーザーに非通知でデータ送信が可能かどうか(データテイント機能の使用有無)を返すメソッド」です。「何それ?」て思われた方、ご安心ください。Netscape Navigator 3.x の時代の、古い古ーい仕様で、データテイント機能は現在使われておりません。多くのブラウザは、ただfalseを返すだけです。
ただしSafari(WebKit)では、このメソッドそのものを定義していません。
だそうで、まんま使わせてもらおう。

<button type="button" onclick="try { alert(window.navigator.taintEnabled == undefined) } catch(e) { alert(false); }">レンダリングエンジンはWebKit?</button>

って、IETester使ってるせいか、デフォルトIE以外だとwindow.navigator.taintEnabledにアクセスしただけで例外吐くのでtry catchで囲まなきゃ。

Safariか、Chromeか。

これはあんまりいい方法が見つからなかったので、両方でSafari5とChrome7の両方でwindowのプロパティを洗いざらい列挙してめぼしいところをいくつか試してみたところ、window.TouchListの有無がよさそうだった。

と思って今試したら、Safariでもwindow.TouchListがundefined返してきた。あれぇ?会社で試したときと違うなあ。

ということで、ちょっと別の方法探さなきゃ。ちなみにモバイル版かの判別は今のところwindow.orientationがNaNじゃなければモバイルってな判定でよさげ。