2010年12月31日金曜日

Pukiwikiでブックマークレットを紹介できるようにする

 通常のPukiwikiのページに「javascript:スクリプト」と直接書いてもリンクにならないし、[[ブックマークレットタイトル>javascript:スクリプト]]と書いてもjavascriptを実行できるリンクにはならない。
 誰でも自由いページを編集できるpukiwikiでデフォルトでそんな設定になっていたら、XSS対策も何もあったものではないからだ。自分の管理するpukiwikiで「file://」をリンクとして扱のと同じよう設定を入れ、「javascript:スクリプト」や「[[ブックマークレットタイトル>javascript:スクリプト]]」をリンクとして扱うようにすることはできるが、インターネットに公開しているサイトではリスクが大きくてする気にもならないだろう。
 悪意のある訪問者が、既存のリンクをjavascriptに書き換え、
「一見通常通りページを開くが、10秒後にスパムサイトがポップアップしてくる」とか、
「リンクをクリックするとスパムURLがついたコメントがPOSTされる」
なんてことを全ページにされたら、気が滅入るどころの話ではない。

とはいえPukiwikiページでブックマークレットを紹介したいニーズもあるので、比較的安全な「ブックマークレット用のリンクを別サイトに表示する」という方法を考えてみた。

 ブックマークレットを紹介したい人は、上記サイトにタイトル1行のスクリプトを入力し、「PukiWiki用リンクの作成」ボタンを押すと[[タイトル>上記サイトのURL?パラメータ]]のようなブランケット形式のリンクを作成し、それを自分のページに張ることで、読者にブックマークレットのリンクを示すことができる。
 読者が張られたリンクをクリックすると、紹介したいブックマークレットのリンクが上記サイトに表示されるというものだ。

読者がリンクをクリックすると下記のようなページが表示される。
このページで実際のブックマークレットのリンクと、確認用に整形されたスクリプトと、お気に入りに登録する方法を確認することができる。

ブックマークレットのリンクの下にはリンク中のjavascriptを見やすく整形したものが表示されるので、読者はその内容を確認したのちに登録することが可能となります。

javascriptを登録できて、このサイト自体のXSSのリスクはどうかという点を心配されるかも知れませんが、このサイトは上記の目的のみで使用されており、置かれているファイルもindex.htmlとjavascript整形用のbeautify.jsの2つだけとなっており、このサイトの改変などを行うことはできません。また、悪意のあるスクリプトを表示させることは可能ですが、このサイトでスクリプトを表示しても、ユーザがリンクをクリックしなければjavascriptは実行されないこと、ユーザは実行またはお気に入りに登録する前に、整形されたスクリプトの表示を確認できることで、リスクの低減になるかと考えています。

サイトの使い方

(1)事前にブックマークレットとするjavascriptを準備し、お気に入りのタイトルを決めておきます。
(2)サイト http://bookmarklet.dip.jp/ を開きます。
(3)「お気に入りタイトル」と「ブックマークレット用1行のjavascript」を入力し、「PukiWiki用リンク作成」を押す
※ここでjavascriptの簡単なチェックを行い、問題があればメッセージが表示されます。
(4)「確認用整形済みコード」で入力したjavascriptが意図したものか確認し、問題がなければ、「pukiwiki用リンク」に表示されたリンク文字列をコピーします。
(5)編集したいpukiwikiページを編集モードで開き、張り付けて使ってください。
(6)張り付けたリンクが正しく動くか確認する

2010年12月30日木曜日

Javascriptで、Javascriptが文法的に正しいか確認する

以下のような関数で、引数の文字列がJavascriptとして正しいか確認できます。
正しい場合false、何か間違いがあれば簡単なエラーメッセージを返します。

// スクリプトに文法エラーがあるか
function checkScript(script) {
  var ret = false;
  try{
    eval('if(0){'+script+'}');
  } catch(e){
    ret = e.message;
  }
  return ret;
}
上記関数は、スクリプトを実行しないように「if(0){}」を付けて実行し、エラーがあるかチェックします。渡されたスクリプトは結局実行されないので、ランタイムエラーの確認はできません。

Textareaでデータとしてjavascriptを入力させたい時などに役に立つかと思います。

ブラウザをFireFoxに限れば、少しコードは複雑になりますが、エラーの発生した行番号も取得することが可能です。少々トリッキーですが、tryの前でエラーオブジェクトをnewしてその行番号を調べることにより、evalの行番号を求め、catch中でeのエラー行の行番号から、evalの行番号を引いた結果に1を足したものが、入力スクリプトのエラー行になります。残念ながらIEではエラーオブジェクトから行番号が取得できないようです。

// スクリプトに文法エラーがあるか(行番号付き)
function checkScript(scr) {
 var ret = false;
 if (!window._lineEval) _lineEval=(new Error()).lineNumber+2 || -1;
 try{
  eval('if(0){'+scr+'}');
 } catch(e){
  ret = {
   line:(e.lineNumber?e.lineNumber-_lineEval+1:-1),
   msg:e.message
  };
 }
 return ret;
}

Javascriptで、Javascriptを整形する

下記のページでJavascriptを整形できる。

http://jsbeautifier.org/

Javascirptを整形してくれるサイトはよくあるが、このサイトはそれをjavascriptで行っている。
サイトに行くといろいろなjavascriptがscriptタグで読み込まれるが、

http://jsbeautifier.org/beautify.js」が整形のエンジンのようだ。

ソースのコメントを見てみると、使いたければ自由に使っていいと書いてある
JS Beautifier
---------------


Written by Einar Lielmanis,
http://jsbeautifier.org/

Originally converted to javascript by Vital,

You are free to use this in any way you want, in case you find this useful or working for you.

これは役に立ちそうだ。ソースのコメントには続けて使い方が書いてある
Usage:
js_beautify(js_source_text);
js_beautify(js_source_text, options);

The options are:
indent_size (default 4) — indentation size,
indent_char (default space) — character to indent with,
preserve_newlines (default true) — whether existing line breaks should be preserved,
preserve_max_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk,
indent_level (default 0) — initial indentation level, you probably won't need this ever,

space_after_anon_function (default false) — if true, then space is added between "function ()"
(jslint is happy about this); if false, then the common "function()" output is used.
braces_on_own_line (default false) - ANSI / Allman brace style, each opening/closing brace gets its own line.

e.g

js_beautify(js_source_text, {indent_size: 1, indent_char: '\t'});

とりあえずは、そのまま呼んでみればいいのね。さっそく使ってみよう。

2010年12月22日水曜日

ブックマークレットでpukiwikiのtracker_listを並べ替え可能にする

ブックマークレットでpukiwikiのtracker_listを並べ替え可能にする

ブックマークレットでpukiwkiの編集中のDiffをとるサンプルを先日示したが、同様に表題のようなサンプルを作成してみた。前回同様、EUC/utf-8、PukiWiki/PukiWikiPlusなどのターゲットの環境を選ばないようにしています。

このブックマークレットでできること

  • pukiwiki及びPlusなどの派生版で、tracker_list、bugtrack_listで表示される表を、ExtJSのGridPanelで置き換えます
  • 置き換えられたGridPanelは、ヘッダ部をクリックすることにより、サーバと通信することなくソート可能です
  • 同じくヘッダ部を操作することにより、列の表示/非表示切り替え、列幅の調整、列の表示位置変更等が可能です
  • 「ページ名」というヘッダを持つ列は、単に文字列によるソートでなく、ページ名に現れる番号を意識して並べ替えられるので、「BugTrack1/100」は「BugTrack1/22」や「BugTrack1/9」よりも後に表示されます((並べ替えの際、スラッシュで区切られる文字列が数字のみである場合、5ケタになるよう前ゼロを付けたのちに比較されます。「BugTrack1/22」は「BugTrack1/00022」に、「ほげ/2/3」は「ほげ/00002/00003」に、比較時に内部的に変換されます))
サンプルの使い方
  • 1.まず下記のURLをブックマーク(IEはお気に入り)に登録します。名前はなんでもいいです。(たとえば「pukiwiki Table」)
javascript:(function(){var%20e=document.createElement('script');e.charset='utf-8';e.src='http://www123.ddo.jp/tools/tracker00.js';document.body.appendChild(e);})()
  • 2.いつも使っているpukiwikiのサイトのBugTrackの画面等を開き、登録したブックマーク(お気に入り)を選びます。
  • 3.すると、上記のようにテーブルがExtJSのgridに変換され、ソートなどの操作ができるようになります

ブックマークレットとスクリプトの解説

上記のブックマークは改行とインデントを入れ読みやすくすると
 javascript:(
     function(){
         var e=document.createElement('script');
         e.charset='utf-8';
         e.src='http://www123.ddo.jp/tools/tracker00.js';
         document.body.appendChild(e);
     }
 )()
見ての通り、これでwww123.ddo.jpからtracker00.jsというスクリプトを読み込んで実行しているわけです。

読み込んだtracker00.jsは下記のような動作をします。

  • ExtJS 3.3.1を動かすために必要なライブラリやスタイルシートを読み込む
  • 最初のtrの要素がtdでなくすべてthであるtableタグについて下記を行います
  •  ヘッダー部の解析とデータストアの作成
  • データの読み込みとGridPanelの描画

下記ページにて確認をしています
セキュリティについて
 
edit00.jsと同様なセキュリティのリスクがあります。理解したうえでお試しください。 

tracker00.jsソースコード 


    // tracker.js v 00 -- pukiwikiのTrackerの機能アップ
    // Bookmarklet example
    //   javascript:(function(){var%20e=document.createElement('script');e.charset='utf-8';e.src='http://www123.ddo.jp/tools/tracker00.js?';document.body.appendChild(e);})()
    // Copyright(c) 2010 mashiki
    // License: GPL version 3
    (function(){
        var cf='http://extjs.cachefly.net/ext-3.3.1/';
        addEl('style', {type:'text/css',innerHTML:'td {background-color:transparent}'});
        addEl('link',{rel:'stylesheet',type:'text/css',href:cf+'resources/css/ext-all.css'});
        addEl('script',{src:cf+'adapter/ext/ext-base.js'});
        addEl('script',{src:cf+'ext-all.js'});
        addEl('script',{src:cf+'examples/ux/TableGrid.js'});
       
        function addEl(tag, cfg) {
            var e=document.createElement(tag);
            for (var key in cfg) e[key]=cfg[key];
            document.body.appendChild(e);
        }
        var id=setInterval(function(){

            if(window.Ext && Ext.MessageBox){
                clearInterval(id);

                var ce = Ext.select('table');
                ce.each(function(tbl){
                    var tr0 = tbl.select('tr', true).first();
                    var hdrs = tr0.query('th');
                   
                    if (hdrs.length>0 && tr0.query("td").length===0 ) {
                        convertToGrid(tbl, hdrs);
                    }
                },true);
            }
        },100);

        function convertToGrid(tbl, hdrs) {
            var ct = tbl.insertSibling(),
                flds = [{name:'no'}],
                cols = [];

            for (var i=0, h; h=hdrs[i]; ++i) {
                var text = h.innerHTML.replace(/]+>(.+?)<\/a>/gi,'$1')
                                    .replace(/\s*(:?↑|↓)\([\d]+\)/,''),
                    name = 'tcol-' + i;
                var fld = {
                    name: name,
                    mapping: 'td:nth('+ (i+1) +')/@innerHTML'
                };
                if (text==='ページ名') {
                    fld.sortType = function(val) {
                        var parts = val.replace(/]+>(.+?)<\/a>/gi,'$1').split("/");
                        for (var i=0,p; p=parts[i];++i) {
                            if (p.match(/^[\d]+$/)) {
                                parts[i]=String.leftPad(p, 5, '0')
                            }
                        }
                        return parts.join('/');
                    }
                }
                flds.push(fld);

                cols.push({
                    header: text,
                    dataIndex: name,
                    width: h.offsetWidth,
                    tooltip: h.title,
                    align: 'left',
                    sortable: true
                });
            }

            var ds = new Ext.data.Store({
                reader: new Ext.data.XmlReader({
                    record: 'tbody tr'
                }, flds)
            });

            ds.loadData(tbl.dom);
            var r = ds.getAt(0);
            if (!(r.get(0))) ds.remove(r);
            i = 0;
            ds.each(function(r){r.set('no',++i)});
            ds.commitChanges();

            ct.setWidth(tbl.getWidth());

            tbl.remove();

            var grid = new Ext.grid.GridPanel({
                stripeRows: true,
                'ds': ds,
                'cm': new Ext.grid.ColumnModel(cols),
                'sm': new Ext.grid.RowSelectionModel(),
                autoHeight: true,
                autoWidth: false,
                renderTo:ct
            });
        }

    })();


2010年12月20日月曜日

ブックマークレットってすごい可能性を持っている と思う

ブックマークレット
Wikipedia 
http://ja.wikipedia.org/wiki/%E3%83%96%E3%83%83%E3%82%AF%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%AC%E3%83%83%E3%83%88

ブックマークレットって実は最近まで注目していなかったのですが、フォームへの半自動入力で使えないかと、知り合いに頼まれて少し調べてみました。

ブックマークレットはブラウザのURL入力欄に書ける1行のjavascriptをブックマークしたものです。たった1行ですが、javascriptなのでDOM操作や、ajax通信などもできます。DOM操作ができるということは、ボタンを表示したり、scriptタグを生成してjavascriptのライブラリを読み込んだり、linkタグを生成してスタイルシートを読み込んだりもできるわけです。
「自分が管理していないが、よく利用しているちょっと不便なサイト」で、ブラウザ上にあるコンテンツを利用していろいろなことができます。

  • フォームの入力を自動で行う
  • 入力のインターフェースを変えたり、入力のチェックを行う
  • 自分の見やすいように表示を並べ替える
  • 他のサイトの情報と合わせて加工して表示する

「javascriptを使えばそんなのできるの当たり前じゃん」と思われますが、自分の管理しているわけでないwebサイトのカスタマイズができちゃうわけです。

サンプルとして、pukiwikiのページの編集で、編集中文書の編集前との差分をExtJSのWindowで表示するブックマークレットを作成してみました。これは標準のpukiwikiにはない機能です。

サンプルの使い方

  • 1.まず下記のURL?をブックマーク(IEはお気に入り)に登録します。名前はなんでもいいです。(たとえば「pukiwiki Diff」)
javascript:(function(){var%20e=document.createElement('script');e.charset='utf-8';e.src='http://www123.ddo.jp/tools/edit00.js';document.body.appendChild(e);})()
  • 2.いつも使っているpukiwikiのサイトのページを編集中に、登録したブックマーク(お気に入り)を選びます。
  • 3.すると、「ページの更新」ボタンの横に「Diff」ボタンが表示されます。
  • 4.「Diff」ボタンを押すと、テキストエリアに何も変更を加えていない場合、「変更箇所はありません」とメッセージが表示され、変更があった場合、次のように変更点が表示されます
    ※このブックマークレットサンプルは、本家のPukiWikiや、派生版のPukiWikiPlus等で、使用可能です。utf-8、EUCのどちらで構築されていても動くと思います。

      ブックマークレットとスクリプトの解説

      見やすさのために改行とインデントを入れると以下のようになります
       javascript:(
           function(){
               var e=document.createElement('script');
               e.charset='utf-8';
               e.src='http://www123.ddo.jp/tools/edit00.js';
               document.body.appendChild(e);
           }
       )()
      見た通り、これでwww123.ddo.jpからedit.jsというスクリプトを読み込んで実行しているわけです。

      読み込んだedit00.jsは下記のような動作をします。

      • ExtJS 3.3.1を動かすために必要なライブラリやスタイルシートを読み込む
      • 'write'という名前のサブミットボタン(ページの更新)があることを確認する
      • 'wirte'ボタンの左に、押されたらdo_diffを実行するような「Diff」ボタンを生成する。
      • do_diffはフォーム中の'original'と'msg'という、フィールドの値を比べ結果を表示する

      edit00.jsのソースは最後に添付します。まあ、http://www123.ddo.jp/tools/edit00.js をDLしソースを確認すればその通りなのですが。

      セキュリティについて

      • http://www123.ddo.jp/tools/edit00.js を信じない方は賢明です。サイトが誰かに乗っ取られ、悪意のあるロジックを混入される恐れはいつでもあります。edit00.jsを信頼できるサイトにコピーして使用いただいて結構です。GPL ver3としますので、適当に改変してください。便利な機能を追加したなら公開していただくとありがたいです。
      • edit00.dsで、cacheflyのサイトからExtJSのライブラリを読み込んでいます。より安全に使いたければ、上記と同様に自分の制御下のサーバに入れることを検討してください。
      edit00.js ソースコード

          // edit.js v 00 -- pukiwikiの編集を支援
          // Bookmarklet example
          //   javascript:(function(){var%20e=document.createElement('script');e.charset='utf-8';e.src='http://www123.ddo.jp/tools/edit00.js?';document.body.appendChild(e);})()
          // Copyright(c) 2010 mashiki
          // License: GPL version 3
          (function(){
              var cf='http://extjs.cachefly.net/ext-3.3.1/';
              addEl('style', {type:'text/css',innerHTML:'td {background-color:transparent}'});
              addEl('link',{rel:'stylesheet',type:'text/css',href:cf+'resources/css/ext-all.css'});
              addEl('script',{src:cf+'adapter/ext/ext-base.js'});
              addEl('script',{src:cf+'ext-all.js'});
              function addEl(tag, cfg) {
                  var e=document.createElement(tag);
                  for (var key in cfg) e[key]=cfg[key];
                  document.body.appendChild(e);
              }
              var id=setInterval(function(){
                  if(window.Ext && Ext.MessageBox){
                      clearInterval(id);
                      var pv = document.getElementsByName('write');
                      if (pv[0]) {
                          pv = pv[0];
                      } else {
                          Ext.Msg.alert(
                              'pukiwiki編集支援機能',
                              '「ページの更新」ボタンが見つかりません。
      '
                              +'この機能はpukiwikiの編集画面でご使用ください'
                          );
                          return;
                      }
                      var df = document.createElement("input");
                      Ext.apply(df, {type:"button",value:"Diff",onclick:diff_click});
                      Ext.get(df).insertBefore(pv);
                  }
              },100);
            
              function diff_click() {
                  var msg = document.getElementsByName('msg');
                  var org = document.getElementsByName('original');
                  if (msg[0] && org[0]) {
                      msg = msg[0].value;
                      org = org[0].value;
                  } else {
                      Ext.Msg.alert(
                          'pukiwiki編集支援機能',
                          '"msg"または"original" フィールドが見つかりません。
      '
                          +'この機能はpukiwikiの編集画面でご使用ください'
                      );
                      return;
                  }
                  if (msg===org) {
                      Ext.Msg.alert('pukiwiki編集支援機能', '変更箇所はありません');
                  } else {
                      var res = diff(
                          org.replace(/^\n+|\n+$/g,'').split("\n"),
                          msg.replace(/^\n+|\n+$/g,'').split("\n")
                      );
                      // 結果加工
                      var map = {
                          '-':{edit:'-',style:'background-color:#FFDDDD;'},
                          '+':{edit:'+',style:'background-color:#DDDDFF;'},
                          '=':{edit:' ',style:'background-color:#FFFFFF;'}
                      }
                      var table = ['
      '];                 for (var i=0; d=res[i]; ++i) {                                        table.push(""                             + "");                 }                 table.push('
      "+map[d.edit].edit+""
                                  + d.arr[d.line].replace(//g,">")
                                  + "
      ');
                      var win = new Ext.Window({
                          title:'pukiwiki編集支援機能',
                          html: table.join(''),
                          width:400,
                          height:400,
                          autoScroll:true,
                          closeAction:'close',
                          plain: true,
                          buttons: [{
                              text: 'Close',
                              handler: function(){
                                  win.close();
                              }
                          }]
                      });
                      win.show();
                  }
              }

              function diff(arr1, arr2, rev) {
                  var len1=arr1.length,
                      len2=arr2.length;
                  // len1 <= len2でなければひっくり返す
                  if (!rev && len1>len2)
                      return diff(arr2, arr1, true);
                  // 変数宣言及び配列初期化
                  var k, p,
                      offset=len1+1,
                      delta =len2-len1,
                      fp=[], ed=[];
                  for (p=0; p
                      fp[p] = -1;
                      ed[p] = [];
                  }
                  // メインの処理
                  for (p=0; fp[delta + offset] != len2; p++) {
                      for(k = -p       ; k <  delta; ++k) snake(k);
                      for(k = delta + p; k >= delta; --k) snake(k);
                  }
                  return ed[delta + offset];

                  // snake
                  function snake(k) {
                      var x, y, e0, o,
                          y1=fp[k-1+offset],
                          y2=fp[k+1+offset];
                      if (y1>=y2) { // 経路選択
                          y = y1+1;
                          x = y-k;
                          e0 = ed[k-1+offset];
                          o = {edit:rev?'-':'+',arr:arr2, line:y-1}
                      } else {
                          y = y2;
                          x = y-k;
                          e0 = ed[k+1+offset];
                          o = {edit:rev?'+':'-',arr:arr1, line:x-1}
                      }
                      // 選択した経路を保存
                      if (o.line>=0) ed[k+offset] = e0.concat(o);

                      var max = len1-x>len2-y?len1-x:len2-y;
                      for (var i=0; i
                          // 経路追加
                          ed[k+offset].push({edit:'=', arr:arr1, line:x+i});
                      }
                      fp[k + offset] = y+i;
                  }
              }

          })();



      2010年12月19日日曜日

      javascriptでdiff実装

      高速とされるO(NP)のアルゴリズムをベースに 、javascriptで実装してみた。
      [1]E.W.Myers, "An O(ND) difference algorithm and its variations", Algorithmixa, 1 (1986), pp.251-266 に論文を日本語に訳したものが載っており、参考になりました。
       http://hp.vector.co.jp/authors/VA007799/viviProg/doc5.htm
      上記の論文中のコードは「SED Shotest Edit Distance」の値を求めるだけで、
      Diffの結果をどう組み立て格納していくかについてと、fpという配列の初期値を
      何で埋めればよいのかが分からず、 かなり苦労しました。

      アルゴリズムは上記のO(NP)そのままですが、javascriptで実装していく中で、
      メイン処理とした親関数内でのループの部分はかなりすっきりさせ、
      snakeへのパラメータは整数kを1つだけとする、snakeは値を返さない、等、
      サンプルコードの原形はほとんどなくなってしまった感じです。

      また、O(NP)はarr1.lengthがarr2.length以下であることが前提なのですが、
      最初の段階でパラメータをチェックし、arr1.lengthがarr2.lengthより大きければ、
      順番をひっくり返して、自分自身を呼ぶことにより、前提を満たすようにしました。

      動くサンプルページ http://www123.ddo.jp/tools/diff.html
      
      //@license http://www.opensource.org/licenses/mit-license.html MIT License
      //@author Mashiki
      //@see http://hp.vector.co.jp/authors/VA007799/viviProg/doc5.htm
      function diff(arr1, arr2, rev) {
          var len1=arr1.length,
              len2=arr2.length;
          // len1 <= len2でなければひっくり返す
          if (!rev && len1>len2)
              return diff(arr2, arr1, true);
          // 変数宣言及び配列初期化
          var k, p,
              offset=len1+1,
              delta =len2-len1,
              fp=[], ed=[];
          for (p=0; p<len1+len2+3; ++p) {
              fp[p] = -1;
              ed[p] = [];
          }
          // メインの処理
          for (p=0; fp[delta + offset] != len2; p++) {
              for(k = -p       ; k <  delta; ++k) snake(k);
              for(k = delta + p; k >= delta; --k) snake(k);
          }
          return ed[delta + offset];
      
          // snake
          function snake(k) {
              var x, y, e0, o,
                  y1=fp[k-1+offset],
                  y2=fp[k+1+offset];
              if (y1>=y2) { // 経路選択
                  y = y1+1;
                  x = y-k;
                  e0 = ed[k-1+offset];
                  o = {edit:rev?'-':'+',arr:arr2, line:y-1}
              } else {
                  y = y2;
                  x = y-k;
                  e0 = ed[k+1+offset];
                  o = {edit:rev?'+':'-',arr:arr1, line:x-1}
              }
              // 選択した経路を保存
              if (o.line>=0) ed[k+offset] = e0.concat(o);
      
              var max = len1-x>len2-y?len1-x:len2-y;
              for (var i=0; i<max && arr1[x+i]===arr2[y+i]; ++i) {
                  // 経路追加
                  ed[k+offset].push({edit:'=', arr:arr1, line:x+i});
              }
              fp[k + offset] = y+i;
          }
      }