[IM]ローカルコンテキストの利用方法

INTER-Mediatorでは、タグのdata-im属性に、「テーブル名@フィールド名」の形式で記述するターゲット指定により、そのタグ要素がデータベースのフィールドにバインドされて、データベースから読み込んだデータを表示し、フォーム用のコンポーネントであればユーザによる変更結果をデータベースに書き戻す事が自動的に設定されます。しかしながら、データベースとは関係ない記憶領域があると何かと便利です。言い換えれば、データベースにしか保持できないとなると設計の順内製がそがれます。そこで、クライアントサイドの計算フィールドを実装し、さらにローカルコンテキストという機能を実装しました。

ローカルコンテキストの利用方法

ローカルコンテキストとは、データベースと連動していない記憶領域で、テーブル名は一律に「_」を使います。フィールド名は任意の変数名でかまいません。Sample_searchでは、type属性がtextのINPUTタグに対して、<input type=”text” data-im=”_@placeCondition”/> という設定を行いました。placeConditionがローカルコンテキスト内の名前です。この記述をするだけで、テキストフィールドに一度入力したデータは、ページを更新しても必ず再現されます。イメージとしてはどこかに記録されて、それが必要に応じて自動的に復活すると考えてください。保存場所はクッキーの中です。

通常のコンテキストは、1つのコンテキストに複数のレコードがあり、そのレコード内にフィールドがあるという構造です。ローカルコンテキストは「レコード」という構造がありません。コンテキスト名も常に決まっているので、実質的にはシンプルなキーバリューストアと同一です。一般のコンテキストは、エンクロージャーとリピーターという繰り返しを誘発する階層構造をベースにしますが、ローカルコンテキストは1ページごとに1つずつ持っており、どこにどう書いてもかまいません。同一のフィールド名のタグ要素が複数あれば、それらでの編集結果も連動します。

ローカルコンテキストへの直接アクセス

テキストフィールドがある場合、そこに入力した値を取り出すのは、通常はgetElementById等を使ってノードを参照します。一方、ローカルコンテキストを利用したテキストフィールドなら、ローカルコンテキストから値を取り出すほうが確実です。たとえば、前述のinputタグ要素の場合、

var c1 = IMLibLocalContext.getValue("placeCondition");

とすれば、テキストフィールドに入れた値を取得できます。なお、テキストフィールドをローカルコンテキストに反映するタイミングは、フォーカスがはずれたとき、つまりonchangeイベントで行っています。もし意図的に値をローカルコンテキストに記録するには、

IMLibLocalContext.setValue("placeCondition", value);

と記述します。

計算式の中に、ローカルコンテキストのリンクノードを参照する記述「_@field」などがあった場合、他のコンテキストにあるフィールドと同様なルートで検索します。つまり、同一のエンクロージャーを探し、なければ上位のエンクロージャを探してその子孫のノードを検索します。

ローカルコンテキストは、URLにひもづくクッキーに記録し、とりあえず、期限は1年にしてみました。いろいろなタイミングで自動的にクッキーに記録し、一方でクッキーから取り出すので、ほぼ、そういうことは意識しなくてもいいのですが、この後に説明する件で、一部のブラウザで書き込みを明示的に記述しないといけない場合があります。

連動するプロパティ

こうして、ローカルコンテキストが稼働しているので、一部のプロパティについては、ローカルコンテキストで保持することにしました。

INTERMediator.startFrom = 0;
INTERMediator.additionalCondition = {};
INTERMediator.additionalSortKey = {};

これらは、前から順番に、検索結果の何レコード目から表示するのか、追加の検索条件、追加のソート条件となります。たとえば、ユーザインタフェースを使って、追加の検索条件を与えると、それがローカルコンテキストに記録されて、事実上永続化されます。検索条件は、随時適用されるので、検索条件を与えるといつまでもその条件が適用されるようになります。

これらの動作に関連する情報は、以下のキー名を使ってローカルコンテキストに保存しています。なお、_im_で始まるキーは、システム予約としておきます。今後、こうした記録が増える可能性があります。

_im_startFrom
_im_additionalCondition
_im_additionalSortKey

たとえば、検索条件を特定のコンテキスト「context」に対して設定する場合、例えば、次のように記述するのは、従来と変わりありません。

INTERMediator.additionalCondition['context'] = {
    field: 'zipcode',
    operator: 'LIKE',
    value: IMLibLocalContext.getValue("criteria") + '%'
};

このとき、背後では、ローカルコンテキストに右辺のオブジェクトを記録し、さらにクッキーへの記録まで同時に行います。

Internet Explorer 8での対処

Ineternet Explorer Ver.8のみ、プロパティに記録したデータを自動的に永続化する事ができません。IE8の場合のみ、INTERMediatorオブジェクトのstartFrom、additionalCondition、additionalSortKeyプロパティに設定した直後に、

IMLibLocalContext.archive();

という呼び出しを入れて、ローカルコンテキストに反映しつつ、クッキーへの保存を行うようにします。IE9以降や、その他のブラウザではこの呼び出しはなくてもかまいませんが、あっても問題はありません。ただし、同じ作業を複数回行うことになるので、効率は低下します。INTER-Mediatorの内部ではIEやそのバージョンの把握を行っているので、たとえば、次のように記述すれば、IE8の場合だけ、前述のメソッド呼び出しが実装できます。

if (INTERMediator.isIE && INTERMediator.ieVersion < 9) {
    IMLibLocalContext.archive();
}

Internet Explorer 8にまた苦しめられる

Web系のお仲間の皆さんも同様にいつも苦しめられていると思われるIE8ですが、ここ最近にあったいくつかのはまりポイントを自分の備忘録としてもまとめておきます。まあ、あと数年はIE8からは逃れられないということで。

  • jQuery 2.0を入れたら動かない(対応ブラウザからはずれている)。慌てて1.10に戻す
  • JavaScriptではObject.keysという記述が使えない
  • 要素のid名と同一のグローバル変数が作られる

特に最後のは苦労しました。メッセージを見る限りは、「オブジェクトでサポートされていないプロパティまたはメソッドです。」と出ます。プログラムはこんな感じ。INPUTタグ要素で、idが「yourname」になっていると思ってください。

yourname = document.getElementById("yourname").value;

まさかgetElementByIdが使えないのかと思ったら、使えます。他の箇所では動いている。valueがない訳は絶対にない。当然右辺を疑うわけですが、必死に検索した結果、問題は左辺でした。つまり、以下の条件が満たされると発生されるトラブルだったのです。

  • IE8でJavaScriptでプログラムを組む
  • ページ内の要素のid属性と同一の変数を、何も定義しないでJavaScript側で使う

どうやら、IE8は、id属性と同一名のグローバル変数を勝手に定義するようです。上記のプログラムは、つまり、勝手に定義しているyourname変数が何らかのオブジェクトを記録していて、そのオブジェクトが書き換えに対応していないためにエラーが出ていると思っていいようです。

対処法は「var yourname」のように頭にvarを付けるか、ページにリンクされたjsファイル等でグローバルとして「var yourname;」のように変数定義することで回避できます。自分自身のグローバルでも、同一名称の変数は後から定義した方のストレージが有効になるので、IEが勝手に作るグローバル変数は無視されるようになるということです。つまり、変数定義を必ずしろというプラクティスをしていれば、エラーに合わないのですが、こう書いてしまったらエラーになってしまうよということですね。

INTER-Mediatorのターゲット指定を汎用化する

INTER-Mediatorでのターゲット指定は、タグ要素のclass属性に、IM[table@field@target] といった形式で記述するものだ。これにより、tableで指定したコンテキストにあるfieldというフィールドのデータを、そのタグ要素に埋め込む。targetを省略すると、フォーム要素以外ではテキストノードとして追加し、フォーム要素ではvalueやcheckedなどの適切な属性に設定される。targetでは、Ver.3.8現在、属性名、innerHTML、nodeText、$target、#target、style.styleNameをサポートしている。

このところ、いろいろなものをINTER-Mediatorで作っていて、ある同じようなパタンが頻繁に発生していることに気付いた。それは、

あるフィールドのデータがあれば表示し、なければ表示しない

という動作である。稀に逆もあるのだが、基本は同じだ。これを実現するために、たとえば、FileMakerだと、計算フィールドを利用する。あるフィールドfieldに対して、計算フィールドdisplayField = if ( field != “”; “block” ; “none” )を定義する。そしてたとえば、こんな感じのHTMLを書く。

<div class=”IM[table@displayField@style.display]”>
データ:<span class=”IM[table@field]”></span>
</div>

SQLデータベースなら、ビューを利用するのがいいだろう。たとえば、こんな感じだろうか?

CREATE VIEW tableview AS
SELECT field, if(LENGTH(field)>0, ‘block’, ‘none’) AS displayField FROM table;

これで対処はできるとは言え、もう少し簡単にならないかと考えてしまう。こうしたよくあることを手軽に対処できるというのはフレームワークに要求されることだ。もちろん、1つの方法は、ターゲット指定の3つ目のパラメータを増やす事だ。たとえば、こんなのはどうだろう?

<div>
データ:<span class=”IM[table@field|table@field@parentVisibility]”></span>
</div>

parentVisibilityという名前の属性はないので、HTMLのルールとのコンフリクトはない。フィールドの値が “” あるいは長さが0であれば、visibilityだったら自分自身、parentVisibilityだったら1レベル上位のノードのdisplayスタイルをnoneにするというところだ。

ということで、機能を増やしました…というのはなんかこの段階に来てやることかなと疑問に考えた。もっと汎用的にすべきではないのか?

そこで考えたのが、次のような仕様である。tagetの代わりに、プログラムのステートメントのような書き方をするということ。たぶん、これがいちばん汎用的と思われる。

table@field@object.method(parameters)

だが、いきなりこれだけですというのは敷居を高くするだけじゃないのかと思われるだろう。もちろん、その解決策はある。object.method(parameters)に対するエイリアスを定義することで、従来通りの記述をサポートするようにする。つまり、…@$onclickや、…@innerHTMLという記述はサポートする。ただし、これらはエイリアスという位置づけにする。ただ、内部的には#や$の扱いが微妙だが、これはif文をがんばって書くしかないだろう。

objectは、次の書き方をサポートしようと考えている。(self) (parent) (enclosure) (repeater) つまり、カッコの中に決められたキーワードを記述する。methodとparametersは任意だ。ただ、parametersは(“a”,’b’)なんて書いたときにカッコ内をそのまま渡すのはセキュリティ上の問題が出そうな感じありありであるので、ここは制約を付けることにする。

ターゲットの3つ目に記述する処理のためのオブジェクトINTERMediatorTargetを作っておく。たとえば、そこに、次のように、setInnerHTMLメソッドがあるとしよう。ここでの関数の最初の引数は、フィールドのデータであるというルールを適用することにする。

INTERMediatorTarget {
setInnerHTML: function(d) {
this.innerHTML = d;
}
}

ターゲット指定は table@field@(self).setInnerHTML() と記述する。そして、[(self).setInnerHTML()」のエイリアスが「innerHTML」であるというわけである。

ノード展開の処理では、ターゲット指定のあるノードnodeに対して、その指定に従ったフィールドのデータfieldDataが得られている。ということは、以下のようにapplyを使えばいいということになる。これは、INTER-Mediator内部の話であって、アプリケーションを作る側は気にしなくてもいい。

INTERMediatorTarget.setInnerHTML.apply( node, [fieldData] );

このノードに展開する仕上げに相当する値をセットする部分が現状ではひどくifの応酬となっているので、まずはそれを緩和したい。また、上記のような仕組みだと、INTERMediatorTargetオブジェクトへのメソッド追加や既存メソッドの書き換えにより、フレームワークの動作を改変することも可能だし、独自のターゲット指定記述を作る事もできる。

INTERMediatorTargetに記述する関数では、前記のように第1引数にフィールド値を設定するのはいいとして、汎用性を高めるために、2、3引数はフィールド名とコンテキスト名にする。これで、コンテキストやフィールドに応じて動作を変える事もできてします。

こういう実装を考えているところである。

[IM]ドラッグ&ドロップでファイルアップロード

ファイルのドラッグ&ドロップによるアップロードをINTER-Mediatorに実装しました。動作条件を整える方法はドキュメントを改めて書きますが、ページファイル上は、

class=”IM[testtable@vc1] IM_WIDGET[im_fileupload]”

のように、IM_WIDGETで、利用するコンポーネントを書くだけで、以下のムービーのようなドラッグ&ドロップによるファイルのアップロードができるようになっています。

dragdrop

[IM]RSAを使って、JavaScriptで暗号化し、PHPで復号する

表題の通りの仕組みを、INTER-Mediatorに組み込もうとして、2日ほどもがきました。RSAつまり秘密鍵と公開鍵を使った暗号化をフレームワークに組み込み、クライアントで入力したパスワードを、暗号化してサーバに送るという仕組みを組み込んでいます。普通パスワードはハッシュだろうと思われるかもしれませんが、フレームワークが認証するパスワードではなく、パスワードをデータベースエンジンへのアクセスに使うべく、クライアントで入力したものをバックエンドまで安全に到達させるための暗号化なのです。

RSAやハッシュ関連のことは、ネイティブな開発ならOpenSSLを使えばおおむねOKなので、同じようにライブラリを集めれば楽勝だろうと思っていました。また、PHPにはopensslライブラリがあるので、まあ、なんとかなるだろうと思いました。要件としては、キーの指定を簡単にするということ。つまり、公開鍵と秘密鍵が1つもPEM形式で得られれば、それを設定ファイルに書き込むことでキーの指定ができるということです。もちろん、生成は「openssl rsagen 1024」みたいなコマンドで簡単に済ませたいわけです。

JavaScriptの世界では、Dave Shapiroさんが作っているライブラリがあり、後々のいくつかのライブラリの多くはこれを改良しています。DaveさんのはPEM形式でキーを与えられないので、Tom WoさんのCrypticoが作られたようで、さらに別の人が署名までできるようにしています。Crypticoでいろいろやっていたのですが、うまくいかない…というか、単純にこのライブラリの問題ではなく、PHPのライブラリとの組み合わせの問題があったのです。

それで、PHPの方はというと、openssl関連関数があるので大丈夫かと思っていたら、これがなかなかうまくいかいないのです。まず、PEMでキーを与えるのが原則とすれば、Crypticoを使ってクライアント側で暗号化するのが手軽です。それを、PHPのopensslの関数で復号するのがどうしてもうまくいかないのです。そのあたりのドキュメントが今ひとつ詳しく書いておらず、コメントには「マニュアルは正しくない…」などと書いてある始末で、あれこれやってもだめでした。

ただし、PHPのopensslの関数を使えば、PEMの鍵データから、RSAの本来の鍵というか、計算式に出てくる鍵の数値をそのまま得られる事がわかりました。さらに、RSA: Encrypting in JavaScript and Decrypting in PHPというドンピシャなものがあったのですが、それはDave Shapiroさんのライブラリを使っていて、RSAの本来の鍵を使う方法が記載されています。ところが、このサイトに書いてあるPHPのライブラリはpearにあるCrypt_RSAで、説明に使っているものはすでのメンテナンスがなされていません。それを使う手もありますが、メンテナンスされているライブラリの方がいいかと思い新しいサイトへのリンクを見ると、なんとこれが、以前このblogでも紹介したPHPだけで作られたSSHのライブラリを含むphpseclib:に移行していたのです。SSHのライブラリはきわめて順調に動いたので期待をして、こちらのRSAを使って復号しようとしました。しかしながら、どんなにこねくり回してもだめなのです。このライブラリのサポートのBBSにあるこの書き込みと同じ場所でエラーが出ているようです。Macだとだめで、debianだと動くようなことが書かれているのが気になるところですが、この方法でうまく流すには簡単には行きそうにありません。

Dave Shapiroさんのライブラリで暗号復号するのはできます。また、phpseclib:で暗号復号はできます。つまり、異なるライブラリをまたいでの暗号復号がうまくいかないのです。Dave Shapiroさんのライブラリで暗号化したものを、opensslコマンドで復号してみると、パディングに関するエラーが出て復号できません。データ形式の微妙な違いがあるのでしょうか? ドキュメンテーションの薄いライブラリや、あまりカスタマイズできないライブラリということで、これ以上手を下すことができず、ここで行き詰まってしまいました。

それで、改めてググって必死に探した結果、見つけたのがbi2phpというライブラリで、しかもMIT Licenseです。JavaScriptのライブラリは、Daveさんのものに独自に改良したものだそうです。それと互換でPHPのクラスを作ったというところでしょうか。いずれも、RSAの本来の鍵を指定するのですが、「データは全部HEX」というように、ある意味非常にわかりやすく、さくっとうまく行きました。

ということで、わかった事をまとめておきます。RSAの秘密鍵、公開鍵という仕組みはもういいとして、そこで使う鍵は、たとえば「openssl genrsa 1024」というコマンド入力で作られます。そのデータはPEMという1つのテキスト列であり、いわば、そこに秘密鍵と公開鍵などなど、RSA暗号処理に必要なデータがPKCSのルールに従ってパックされたものです。

$ openssl genrsa 1024
Generating RSA private key, 1024 bit long modulus ...................++++++ ............++++++ e is 65537 (0x10001) -----BEGIN RSA PRIVATE KEY----- MIICXwIBAAKBgQC/BlONnPUfSc95YmrcOUV0IbmeBZvibbAssetKBXAG0DGeKzc7        : 2MgfcIZ3C7lf0+yx3/RhXwJBAIYHkht7UPSpPeTvPzc4v89yBlkkGeN9xLbdONT3 uzINAQkGvVmDhNLYqxkgDysBUy/Q2f41DenUZJfEFLQBs5w= -----END RSA PRIVATE KEY-----

PHP側では、このPEM形式の文字列を使って、以下のプログラムで、RSAの計算に使うキーを求めることができます。$generatedPrivateKeyにはPEM形式のキーの文字列、圧縮アルゴリズムがAESの場合はパスフレーズを入れるので、その場合は$passPhraseに指定しますが、パスフレーズが設定されていない鍵は2つ目の引数は適当に無視します。

$res = openssl_pkey_get_private( $generatedPrivateKey, $passPhrase );
$keyArray = openssl_pkey_get_details($res);

この、$keyArrayの配列にいろいろなものが入っていて、公開鍵だけを取り出すには、$keyArray[‘key’]とします。

Daveさんのライブラリを使うには、鍵のオブジェクトを作成しますが、当然、自分で生成するのではなく、このPEMを鍵としてオブジェクトを初期化します。初期化関数部分は、次のように定義されています。biRSAKeyPairが鍵のクラス名と考えていいでしょう。

function biRSAKeyPair(encryptionExponent, decryptionExponent, modulus)

ここで、encryptionExponentは$keyArray[‘rsa’][‘e’]、decryptionExponentは$keyArray[‘rsa’][‘d’]、modulusは$keyArray[‘rsa’][‘n’]から得られます。生成した鍵からopensslのコマンドを使って出力(例えば、openssl rsa -text -in gen.key)した結果を見ながら、対応付けを確認しました。配列から得られた結果はバイナリなので、PHPの場合、bin2hex関数を使って16進文字列にして、JavaScript側の引数に指定をします。2次元目のeとdは、encrypt、decryptなんでしょうね。

なお、INTER-Mediatorでは、JavaScript側では暗号化しかしないので、2番目の引数は指定しません。というか、指定してクライアント側で見えたらいけません。なぜなら、$keyArray[‘rsa’][‘d’]は秘密鍵であり、暗号化には使わないからです。いずれにしても、JavaScriptのプログラムをサーバ側のPHPで生成してクライアントに送り込むという部分がこうした処理を組み合わせて作る事ができます。

bi2phpのPHP側のクラスは、JavaScriptとほぼ同様なプログラムでいいようになっており、サンプルのHTMLを見ればプログラムの作り方はすぐにわかると思います。非常に紆余曲折がありましたが、JavaScriptとPHPの双方で暗号復号をするには、bi2phpというライブラリでOKということです。ただし、PHPのopenssl関数を使って、PEMから鍵の値を得たりということは処理として必要になるということです。このライブラリをまとめたAndrey Ovcharenkoさんに感謝するともに、ライブラリをここまで育て上げたみなさんに感謝します。

JavaScriptのeventではまる

キーコードを取るkeyCodeや、シフトキーを押しているかどうかを確かめるshiftKeyというプロパティが「event」で使えることになっている。そこで、

document.onkeydown = function(e) { .. }

とすれば、常にキーを押したことを拾えるが、この場合は仮引数eがイベントを参照しており、e.keyCodeでキーコードが拾える。

ところが、次のように、onclickに関数を書いた場合、ブラウザ間の非互換性が発生する。

function clickThis(target) { … }
:
<div onclick=”clickThis(this)”></div>

clickThis関数内で「event.keyCode」と書いて動いてると思っていたら、Firefoxのみうまく動かなかった。つまり、eventというキーワードで、直前のイベントを取得するのはFirefoxではできなかったということである。Safariではできた。

しかしながら、以下のようにすると、Firefoxでも稼働した。関数clickThis内部では、ev.keyCodeなどによってイベントのプロパティに参照する。

function clickThis( target, ev ) { … }
:
<div onclick=”clickThis( this, event)”></div>

つまり、Firefoxでは、「event」によって、直前のイベントを参照できるのはローカルな範囲だけということになる。他のブラウザで、「event」という記述が使えるのは「window.event」というウインドウに直前のイベントを参照できるプロパティが存在しているからである。Firefoxにはそういうプロパティは存在しないが、JavaScriptの規約では存在しないので正しいらしい。