これでできる! クロスブラウザJavaScript入門

第9回 実践DOMスクリプティング#2:DOMとHTML

この記事を読むのに必要な時間:およそ 4.5 分

こんにちは,太田です。前回はHTMLとテキスト操作を解説しました。今回は,さらにHTMLの操作の実例を中心に解説していきます。

JavaScriptとエスケープ

まず,エスケープについて確認しておきましょう。外部から入力されたデータを画面上に表示する際はクロスサイトスクリプティングに注意する必要があることはもはや常識と言ってよいと思います。サーバーサイドのウェブ開発用フレームワークでは,ほとんどの場合HTMLのエスケープ用のメソッドが用意されていて,HTMLのエスケープは適切に行えるはずです。しかし,注意しなければいけないのは,HTMLのエスケープはJavaScriptにそのまま当てはめることはできないという点です。当たり前の話ですが,HTMLとJavaScriptではエスケープすべき文字が異なります。また,IEの6,7ではCSSにJavaScriptを埋めこむことができるのでCSSにも注意が必要です。

さて,実際にどのようにエスケープすればよいのでしょうか。JavaScriptの仕様,文字コードの問題などを含めて自分なりに実装してみるというのはお勧めできません。こういった場合,既に用意されている方法があるならそれを使うべきです。

その具体的な方法の1つがJSONです。最近では多くの言語がJSONをネイティブにサポートしていますし,ネイティブサポートしていなくともライブラリなどはすぐに見つけられると思います。JSONはJavaScriptから生まれたデータ記述フォーマットで,真偽値,数値,文字列,null値の組み合わせを持ったハッシュか配列かその両方で構成されます。関数や正規表現などはJSONには含まれません。よって,適切にJSONとして出力されたデータは安全なデータといえます(ただし,文字コードによってはデータを壊して読み取る方法が存在しないこともないので,JSONを単体で出力する際にはContent-Typeで文字コードを指定することを忘れないようにする必要があります)⁠

ただし,JSONとして出力したものをHTMLに埋め込む場合はさらに注意が必要です。例えば次のようにHTML中にJSONを出力する場合,scriptの閉じタグが含まれればそこが閉じタグになってしまいます。

scriptの閉じタグ

<script>
var data = {"a":1,"b":[1,2,3],"c":{"d":"XXX</script><script>"}};
</script>

この場合は当然HTMLとしてのエスケープも必要で,次のように出力する必要があります。

scriptの閉じタグのエスケープ

<script>
var data = {"a":1,"b":[1,2,3],"c":{"d":"XXX&lt;/script&gt;&lt;script&gt;"}};
</script>

この場合は/の前にバックスラッシュを入れることで閉じタグとして解釈されることを回避できますが,script要素を<--でコメントにしていた場合は,-->でコメント部分をそこで終了させることができてしまいます。こういった複雑さから,scriptは動的に作らないというのがまず基本として,どうしても値を渡したい場合はinput要素のhiddenの値として埋め込んだり,数値や真偽値などの特定の値に限定するようにしたほうがよいでしょう。

なお,前回のキーワードハイライトで使用したXPathでも,ユーザーの入力を元にクエリを作っていたので正確にはエスケープ処理が必要でした。XPath1.0でのエスケープ処理は少々厄介ですが,XPath に文字列を埋め込むときの注意 - IT戦記のescapeXPathExpr関数を利用することで正しくエスケープ処理をすることができます。

HTMLの出力方法

さて,JSONとして埋め込んだデータを画面に出力する方法をすこし考えてみます。まずはDOMのメソッドで要素を作る方法です。createElementで要素を作り,テキストはcreateTextNodeで作って,それぞれを順番にappendChildで追加する方法です。この方法でTwitterの検索APIからHTMLを組み立てるコードは次のとおりです。

DOMのメソッドによるHTMLの組み立て

function TwitterCallback1(data){
  var div = document.getElementById('twitter-search-r1');
  /* 前回の結果がある場合に削除 */
  while (div.firstChild) {
    div.removeChild(div.firstChild);
  }
  var results = data.results;
  var ul = document.createElement('ul');
  ul.className = 'twl';
  for (var i = 0, len = results.length;i < len; i++){
    var usr = results[i];
        var user = usr.from_user;
    /* 要素を作る */
    var li = document.createElement('li');
    var link = document.createElement('a');
    var icon = document.createElement('img');
    var name = document.createElement('span');
    var entry = document.createElement('p');
    var time = document.createElement('div');
    var timelink = document.createElement('a');
    /* CSS用にclassを設定 */
    li.className = ((i+1)%2) ? 'odd' : 'even';
    link.className = 'usr';
    entry.className = 'entry';
    time.className = 'time';
    /* リンクや画像などの属性を設定 */
    link.href = 'http://twitter.com/' + user;
    var src = usr.profile_image_url;
    if (src.indexOf('http') === 0) {
      icon.src = src;
    }
    icon.width = 48;
    icon.height = 48;
    timelink.href = 'http://twitter.com/' +
                user +'/status/' + usr.id;
    var d = new Date(usr.created_at);
    var date = d.getFullYear() + '/' + (d.getMonth()+1) +
           '/' + d.getDate() + ' ' + d.getHours() + ':' +
           ('0'+d.getMinutes()).slice(-2);
    /* テキストノードの挿入 */
    entry.appendChild(document.createTextNode(usr.text));
    timelink.appendChild(document.createTextNode(date));
    name.appendChild(document.createTextNode(user));
    /* 要素の組み立て */
    link.appendChild(icon);
    link.appendChild(document.createElement('br'));
    link.appendChild(name);
    li.appendChild(link);
    li.appendChild(entry);
    time.appendChild(timelink);
    li.appendChild(time);
    ul.appendChild(li);
  }
  /* 画面に反映 */
  div.appendChild(ul);
}
var b1 =document.getElementById('twitter-search-b1');
b1.onclick=function(){
  var script = document.createElement('script');
  script.src = 'http://search.twitter.com/search.json'+
    '?callback=TwitterCallback1&lang=ja&q=JavaScript';
  document.body.appendChild(script);
}

この方法は,属性には文字列を設定しているだけですし,テキストはテキストノードとして挿入しているので,万が一TwitterのAPIのエスケープ処理が不十分でHTMLタグが混入したとしてもクロスサイトスクリプティングは成立しません。

しかし,appendChildで要素を順番に挿入してHTMLの構造を組み立てているところなど,実際にどういったHTMLができあがるのか見通しが悪く,メンテナンスの面ではかなり問題があります。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/

コメント

  • display:none; 方式の欠点

    > <ul id="twitter-search-t2" style="display:none;">

    この方式だと、「CSS-Off & JS-Off」のUAで display:none; を指定した要素が見えてしまいます。
    一方、「DOMのメソッドによるHTMLの組み立て」はCSSに依存しないので、JavaScriptで使われる要素は表示されません。
    両コードのいいとこ取りをすると、

    <ul id="twitter-search-t2">
    <li>CSS-Off時に表示するコンテンツ</li>
    </ul>
    <script>
    var element = document.getElementById('twitter-search-t2').cloneNode(true);
    // 要素を組み替えて、element.replaceChild()
    </script>

    コードが冗長になってしまうために「あえて、display:none; 方式にしたのかな」とも思いますが、一応書いておきます…。

    Commented : #1  think (2010/07/06, 23:10)

コメントの記入