DOMDOMタイムス#13: プロトタイプ汚染環境で、キレイなプロトタイプをiframeから引っこ抜く
3週間ぶりのDOMDOMタイムスです!!!!
先々週、先週は下記の「webページ崩し」の作成に邁進していましたので……(お気に入りだから動画も貼っちゃう)
twitterで良い反応がもらえたので、これもいつかリリースしたあかつきにはちゃんと記事にしようと思います👶
さて今日はあいだがあいてしまったのでちゃんと実践的なDOMDOMトークをしていくザマス。
(DOMってよりけっこう幅広く使える話かも??)
まとめ
- プロトタイプはなんだかんだ汚れがち(polyfillはもちろん、ライブラリ(例:Zone.js)が汚してくることもやっぱりある)
- プロトタイプが汚染されまくっているコンテクスト上できれいなプロトタイプが得たいなら、iframeを作ってそこから引っ張り出すといい
プロトタイプの書き換えって意外とあるヨネ
プロトタイプの書き換えが行われているwebページって意外とあるよね。
例えばGoogle Cloud(https://console.cloud.google.com/)を見てみます。
Promise
やDocument.prototype.querySelectorAll
がガンガンに書き換えられていることがわかりますね👶
一方でShadowRoot
はネイティブのままっぽいです。
他のページも色々確認してみると、案外出てくるものです。
このような書き換えは、みんな大好きpolyfillによっても起こりうるし、その他のライブラリの使用によっても起こりえます。
(例えば有名なZone.jsには、自身によるPromiseの書き換えが成功したかを判定するコードがあり、なんか書き換わっているサイトがたしか前あった(忘れちゃったけど): https://github.com/angular/zone.js/blob/b11bd466c20dc338b6d4d5bc3e59868ff263778d/lib/zone.ts#L707)
汚染されていないプロトタイプをiframeから取得する
じゃあ書き換えられていないフレッシュなプロトタイプがほしいときどうしよう??
やり方
答えはカンタン、iframeを作ってそこから引っ張り出しちゃおう!
const iframe = document.createElement('iframe')
// iframeはDOMツリーに接続して初めてDocumentを作ってくれるので接続する
iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);
// iframeから取ってくる
const originalPromise = iframe.contentWindow.Promise
// iframeを消す
iframe.remove()
さっきのGCPのページでやってみるとこんな感じになりモス👶いいね〜!
関数だけとってくる方法
ちなみに、関数だけでいいからキレイなやつがほしい!という人向けにも、一応下記のようなコードを載せておいてみます。
(document
から呼び出せるのはDocument.prototype.querySelectorAll
であって、Element.prototype.querySelectorAll
を引っ張ってきてもダメなことに注意!!)
const iframe = document.createElement('iframe')
// iframeはDOMツリーに接続して初めてDocumentを作ってくれるので接続する
iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);
// iframeから取ってくる
const originalDocumentQuerySelectorAll = iframe.contentWindow.Document.prototype.querySelectorAll
// iframeを消す
iframe.remove()
// callやapplyでむりやり使っちゃおう!
// 例えばこれはdocument.querySelectorAll('div')と同じだね
originalDocumentQuerySelectorAll.call(document, 'p')
// bindで保管しておくのも場合によっては便利かも?
const documentQuerySelectorAll = originalDocumentQuerySelectorAll.bind(document)
documentQuerySelectorAll('div')
getterやsetterでもできるヨ
getterやsetterでも同じことができますが、少し工夫が必要です。
例えばElement.prototype.children
でやってみましょう。
// iframeを作ったあと、こうやると失敗する
const originalElementChildren = iframe.contentWindow.Element.prototype.children
// => Uncaught TypeError: Illegal invocation
この書き方ではElement.prototype
を呼び出しコンテキストとしてgetterchildren
を実行していることになっちゃうんですね。
いまはgetterchildren
の本体がほしいので、Object.getOwnPropertyDescriptor
で無理やり抜き取りましょう🌞
const originalElementChildren = Object.getOwnPropertyDescriptor(Element.prototype, 'children').get
// => children() { [native code] }
あとはこれをcallやapplyでむりやり使えばよいね!
というわけでiframeの作成からすべてまとめて書くとこんな感じね。
const iframe = document.createElement('iframe')
// iframeはDOMツリーに接続して初めてDocumentを作ってくれるので接続する
iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);
// iframeから取ってくる
const originalElementChildren = Object.getOwnPropertyDescriptor(iframe.contentWindow.Element.prototype, 'children').get
// iframeを消す
iframe.remove()
originalElementChildren.call(document.documentElement)
補足: 汚染されているかの判定
ところで関数の場合は、関数をiframeからぶっこ抜いてくるべきかの判断のために、関数がそもそも汚染されているかの判定があると便利かもしれないんだけど、それはこのあたりを見てみてね。
if( /\{\s+\[native code\]/.test( Function.prototype.toString.call( this[ p ] ) ) ) {
// yep, native
}
ちなみに👆って要するに、native code
という文字列が出るかで判定しようって話なんだけど、実はこれちゃんとECMAScriptのFunction.prototype.toString()
の仕様に準拠していますヨ👶
Function.prototype.toString()
がnative code
という文字列を含んだ結果を返すのは仕様なんですねえ。
おわりに
まあそりゃ一番いいのはShadow RealmがESに実装されることなんだけどね!
でももうしばらくなさそうらしい、そんな話をTC39のイベントで耳にしました🌝
(実装がきついよって話がgithub上で議論されているらしいので、気になる人は見てみよう!!)
今日はここまで!
参考
本稿は下記の記事にインスパイアされています!
Discussion