タイトル/名前 | 更新者 | 更新日 |
---|---|---|
技術/CVS | msakamoto-sf | 2008-12-16 00:28:29 |
PHP/「ページの有効期限切れ」対策 | msakamoto-sf | 2008-12-16 00:03:24 |
添付ファイル/PHP/session03.log | msakamoto-sf | 2008-12-16 00:02:42 |
添付ファイル/PHP/session02.log | msakamoto-sf | 2008-12-16 00:02:15 |
添付ファイル/PHP/session01.log | msakamoto-sf | 2008-12-16 00:01:34 |
画像/PHP/re_post_confirm.jpg | msakamoto-sf | 2008-12-16 00:00:25 |
日記/2008/12/13/IEのshowModalDialog内からの遷移でCookieが渡らない件 | msakamoto-sf | 2008-12-13 02:19:40 |
添付ファイル/2008/12/13/021722/kb831678_php_sample.zip | msakamoto-sf | 2008-12-13 02:17:59 |
日記/2008/12/04/ifブロックのコメントの付け方 | msakamoto-sf | 2008-12-04 00:32:37 |
日記/2008/11/30/「プロジェクトマネージメント」についての私感 | msakamoto-sf | 2008-11-30 10:26:28 |
CVSメモ。主にCVSを使っていて、嵌ってしまったところや一般的な使用方法の個人用メモなど。
CVS自体の使い方は、下記が今のところ一番安定しているドキュメントか?
http://www.linkclub.or.jp/~tumibito/soft-an/cvs/cvs-man/
バージョン管理システムと言えば、直観的で分かりやすいUIを備えているMSのVisualSourceSafe(VSS)しか会社で使ったことが無かった。しかしUNIX/Linuxでのソースコード管理と言えば CVS と言うことで、実は2003年(*1)の時点から勉強はしていた。
・・・が、どうにも感覚が掴めないのが多く、中々使いこなせなかった。ようやく、頭の中で主要なCVSの概念が焦点を結んだので、そのあたりをメモしておく。
特に断りがない場合、"CVS"と表記するとUNIX/Linux/CygwinのCVSツール全般のこととする。
CVSは カレントディレクトリ指向 のツールである、と思う。どういう事かというと、
$ cd /your/foo/bar/ $ cvs (cvsコマンド)
とすると、"/your/foo/bar" ディレクトリに対していろいろ操作する。 コマンドライン引数として、「操作対象のディレクトリ」を持たない。 さらに言うと、CVSの管理対象としてもっとも上位の単位になる「リポジトリ」であるが、これも環境変数CVSROOTで指定するようになっており、コマンドラインからは指定できない。(*2)
カレントディレクトリを対象とするとして、ではどうやってリポジトリ位置やモジュール名を判別しているのか?それが、"CVS"ディレクトリであり、この中のRepositryやらRootやらを勝手に探索して勝手に判断してくれているだけである。
これを肌で知っていないと、 importで思いっきり嵌る。
importは、 カレントディレクトリの中身 を指定されたモジュール名で登録するコマンドということ。
モジュール名 というのもイヤラシイ用語だが、要はCVSROOTで指定したディレクトリの直下のディレクトリと考えて構わない。 CVSROOT というのにもかなり振り回された。
例えば、リポジトリを作成するコマンドをみてみる。
$ cvs -d /your/cvs/repositry init
これを実行すると、
/your/cvs/repositry/ CVSROOT/
という具合になる。CVSROOTがある。・・・が、 環境変数 CVSROOT で指定するのは"/your/cvs/repositry"までなのだ。
何度、「"CVSROOT"だから".../repositry/CVSROOT"まで指定するんだ」と間違えたかことか!!
間違えてはいけない。 リポジトリの下にある"CVSROOT"は単なるモジュールである。
/your/cvs/repositry/ : こちらがCVSROOT環境変数に使用する「リポジトリ」の位置 CVSROOT/ : これはCVSROOTという名前のモジュール
CVSROOTモジュールは、META-INFとかにした方が紛らわしくなくて良いのでは?とか思うが、ようするにそういった扱いのモジュールなのだ。リポジトリ全体の設定を行う為、これはこれで、CheckOut/CheckInのできるモジュールとして管理できるようになっている、というわけである。
環境変数としてのCVSROOTには何種類か書き方があるが、現在、実際に利用されているのは次の二種類だろう。
CVSROOT=/your/cvs/repositry (これは CVSROOT=:local:/your/cvs/repositry と同義) CVSROOT=:ext:user@cvs.sourceforge.net:/cvsroot/yourproject
このように、リポジトリの位置はあくまでもCVSROOTで決まる。・・・にしても、つくづく紛らわしい。
$ CVSROOT=:ext:user@cvs.sourceforge.net:/cvsroot/yourproject $ cvs co CVSROOT
とかやられた日には、何がなんだか分からなくなる・・・というか、分からなかった。
CVSNTはともかく、Linux/Unix系であればCVSはSSH越しが一般的な使用方法だろう。ここも嵌りやすい。特にWindowsだと、SSHのクライアントとして PuTTY や Cygwin のSSHなど幾つかあるからだ。
重要な点は、CVSコマンドとSSH通信のレイヤーは完全に分離されている 点である。
元々CVSはリモートサーバにログインしてCVSを叩く、RSHが使えた。これが、CVS_RSHという環境変数の由来。詰まるところ、リモートログインできるシェルアプリであれば、PuTTYであろうとCygwinのsshであろうと何だって良いのだ。共有鍵を作るのも、これまた何で作ろうと構わない。TeraTerm(+TTSSH)で作成した公開鍵を、PuTTYの鍵作成ツールでPuTTY用に変換しても良い。あるいは最初からCygwinのOpenSSHで作成しても構わない。
特に自由度が高いのは、CygwinでインストールしたCVSを利用するとき。OpenSSH(Cygwin)とPuTTYを入れていれば、
CVS_RSH=ssh
でも、
CVS_RSH="/cygwin/c/Program Files/PuTTY/plink.exe"
でも、どちらでも構わないのだ。
で、嵌りやすいのがCVS_RSHで指定したツールで 初めて接続するとき 。大概、サーバー側からの公開鍵を受け付けるか聞かれるプロンプトが表示される。なので、理想的にはCVS_RSHで指定したツールで前もってCVSサーバに、普通にログイン・ログアウトしておくこと。こうしておけば、その時にサーバの公開鍵が登録されるので、CVS側で接続したときに正体不明のプロンプトらしき文字列に悩まされることはない。
様々な書籍やWeb上のリンクが豊富にある為、基本的な利用法は特に書かない。
リポジトリの位置はあくまでもCVSROOT環境変数ので決まる。これ、.bash_profileとかに書くと、複数のリポジトリを併用する場合煩わしい。
他のシーンでも自分がよく使っている手として、setenv.shというのを作成しておき、切り替えて使う、というのをメモしておく。以下の様なsetenv.shを、ローカルにcheck outしたディレクトリの一つ上とかに作成しておき、切り替えられるようにしておく。あるいは setenv_(project名).shとかにしておいても良い。
#!/bin/sh unset CVSROOT CVSROOT=:ext:user@your.cvs.server:/your/cvs/repositry
例えば、/your/work/dir 以下で、server1 と server2 のそれぞれのリポジトリのモジュールで作業しているとき、次の様なディレクトリレイアウトを整えておく。
/your/work/dir/ setenv_server1.sh setenv_server2.sh server1/ moduleA/ CVS/ ... server2/ moduleB/ CVS/ ... ---------------------------------- [setenv_server1.sh] #!/bin/sh unset CVSROOT CVSROOT=:ext:user@server1:/your/cvs/repositry ---------------------------------- [setenv_server2.sh] #!/bin/sh unset CVSROOT CVSROOT=:ext:user@server2:/your/cvs/repositry
で、実際に使うときはsetenv_serverX.shを取り込む。
$ cd /your/work/dir $ . setenv_server1.sh $ cd server1/ $ cvs co moduleA $ cd moduleA/ $ cvs update
で、server2のリポジトリに対して作業したい場合は、別のターミナルなりを立ち上げて、
$ . setenv_server2.sh
とすれば良い。
特にIE系でよく見られる「ページの期限切れ」画面。これを発生させないためにはどうすればよいのか、現在は少しGoogleで検索するだけで実に様々な対策方法が蓄積されている。だが、そもそも「ページの期限切れ」とはいったい何を示しているのか?いったいこの画面はユーザーに何を訴えているのだろうか?
今回はPHP言語に限定して、この現象を可能な範囲その原因を追及し、抜本的対策と巷間にあふれる対策方法の是非を検討する。最終的に必要となった知識はHTTPのRFC2616のキャッシュ機能およびPHPのext/session/session.cのソースコードとなった。
まず現象を再現するところから始める。「ページの有効期限切れ」画面が表示されるのはIE系なので、以下に示す各スクリプトもIEで表示させることを前提とする。Firefoxの場合の動作は後述する。
<?php session_name('CacheExpireExperiment01'); session_start(); ?> <html> <body> <form action="" method="POST"> <input type="text" value="" name=""> <input type="submit"> </form> </body> </html>
<?php session_cache_limiter('nocache'); session_name('CacheExpireExperiment02'); session_start(); ?> <html> <body> <form action="" method="POST"> <input type="text" value="" name=""> <input type="submit"> </form> </body> </html>
<?php session_cache_limiter('private'); session_name('CacheExpireExperiment03'); session_start(); ?> <html> <body> <form action="" method="POST"> <input type="text" value="" name="test"> <input type="submit"> </form> </body> </html>
<html> <body> <form action="" method="POST"> <input type="text" value=""> <input type="submit"> </form> </body> </html>
「ページの有効期限切れ」画面は主にIE系で取り上げられる現象である。一方のNetscape系(Geckoエンジン系)のブラウザではあまりそう言った話題はあがらない。では、実際どうなるのか?Firefox1.04で前掲のsession01/02.phpにアクセスし、IEの時と同様の手順を踏んでみる。
結論として、「再度POSTして良いですか?」という下図に示すような画面が表示され、OKをクリックすると再度サーバーにリクエストが送られている。(この段階ではあくまでも送られている「らしい」までしか目視確認できないが、後述のFirefoxのLiveHTTPHeadersの解析により実際にリクエストが送られていることを確認できた。)
IE/Firefoxとも、発生しないスクリプトの場合特にサーバーにアクセスも発生せず、表示の早さとフォームに入力された値を覚えてくれている辺り(実際、各Submit毎に入力した値を完全に覚えていた)も同様である。どうやらブラウザのキャッシュにアクセスしているらしい。
発生するスクリプト・しないスクリプトの違いはsession_cache_limiter()の違いである。発生するスクリプトはsession_cache_limiter()を呼んでいないか、'nocache'を渡している。実際にPHPのマニュアルを参照してみる。
http://jp.php.net/manual/ja/function.session-cache-limiter.php
これによると、この関数はHTTPヘッダーを操作し、クライアントに対してキャッシュ制御を行う関数らしい。引数はマニュアルを読む限り次の4つ。
当関数を呼ばない場合、自動的にsession.cache_limiterに指定された値が適用されるらしい。そのデフォルト値は'nocache'であるらしい。従って、前掲のsession01.phpは当関数を呼んでいないため'nocache'が仮定され、結果として動作はsession02.phpと同等のものになっていたことが推測される。
session_cache_limiter()はHTTPヘッダーを操作する。従って、前掲のスクリプトの動作を解析するにはそのHTTPヘッダーを観察する必要がある。今回はFirefoxの拡張(extension)の一つであるLiveHTTPHeadersを用いて、前掲の三つのスクリプトにアクセスした場合のHTTPヘッダーを観察してみることにした。
session01 - 03.phpにアクセスする。いずれもアクセスの前にCookieとブラウザキャッシュを全クリアする。操作としては以下の手順で統一した。
このときのLiveHTTPHeadersのヘッダーを以下のファイルに保存した。
以下にログファイルを解析した結果を、要点を絞ってまとめる。
サーバーからの応答の内、Expires, Cache-Control, Pragma 辺りが怪しい。
HTTP/1.x 200 OK Date: Sat, 09 Jul 2005 15:03:38 GMT Server: Apache/2.0.50 (Win32) PHP/4.3.8 X-Powered-By: PHP/4.3.8 Set-Cookie: CacheExpireExperiment01=225b4c965b29028b78bec94bfebce4cb; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=EUC-JP
HTTP/1.x 200 OK Date: Sat, 09 Jul 2005 15:04:21 GMT Server: Apache/2.0.50 (Win32) PHP/4.3.8 X-Powered-By: PHP/4.3.8 Set-Cookie: CacheExpireExperiment02=56e20d4184fbe8690b9e1beb00d314e1; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Keep-Alive: timeout=15, max=99 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=EUC-JP
HTTP/1.x 200 OK Date: Sat, 09 Jul 2005 15:04:51 GMT Server: Apache/2.0.50 (Win32) PHP/4.3.8 X-Powered-By: PHP/4.3.8 Set-Cookie: CacheExpireExperiment03=ef639f33f6259fea6437c5aa3255a429; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: private, max-age=10800, pre-check=10800 Last-Modified: Sat, 09 Jul 2005 15:00:52 GMT Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=EUC-JP
まず、session01と02において、Expires/Cache-Control/Pragmaヘッダーフィールドが完全に一致している。これは前述のsession_cache_limiter()とsession.cache_limiterのデフォルト設定との関連より推測した、session01と02の動作が同じであることを証明している。
続いてsession03.phpとの比較だが、session03.phpで大きく異なるのが次の部分である。
Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: private, max-age=10800, pre-check=10800 Last-Modified: Sat, 09 Jul 2005 15:00:52 GMT
Cache-Controlが no-cache ではなくなり、privateになっている。また幾つかのパラメータが追加されている。さらにLast-Modifiedフィールドが追加されている。ちなみに、session_cache_limiter()にprivate_no_expireを指定するとExpiresフィールドが無くなる。この点についてはPHPのマニュアルと一致している。
以上より、session_cache_limiter()に渡す値は Cache-Control, Pragma のヘッダーフィールドに影響を与える事が確認できた。ではこの二つ(およびExpires, Last-Modified)ヘッダーフィールドが、ブラウザにどういった影響を与えるのかをHTTPのRFC2616を元に調べてみる。
HTTPについての文献は次のURLに日本語で非常に詳しくまとめられている。以下、このHPを元に今回の調査対象となるヘッダーフィールドおよびブラウザのキャッシュ制御について著者の視点からまとめてみる。
注意 :日本語訳の使用上の注意点については必ず http://www.studyinghttp.net/translations#Notice を参照して下さい。あくまでも英語文書が正式版であり、日本語訳した場合の誤訳や誤解については無保証だそうです。
Webページがキャッシュされる場所はクライアントという視点では二カ所ある。
プロクシサーバーは一般に共用サーバーである。この事により、「個人のプライバシーに関わるページのキャッシュの是非」を操作できる必要性が生じる。これにより、キャッシュには主に以下の3段階の"レベル"が導入されるべきである。
さらに重要な点として、「キャッシュが無効化されるのをいつ、どのようにしてブラウザは認識するのか」という問題も出てくる。すなわち、
の二点を制御できて初めてキャッシュを安全に確実に利用できるようになる。
現在主なサーバー・ブラウザ(クライアントプログラム)が対応しているHTTPのバージョンは 1.1/1.0 の二種類。このうち、HTTP/1.0時代のキャッシュ制御ヘッダーフィールドについてまとめる。
Expires, Pragma ともにHTTP/1.0時代のフィールドだが、下位互換性のため使用している。HTTP/1.1に対応したプログラムの場合はCache-Controlヘッダーフィールドがこれらより優先して使用される。
近年の主要なサーバー・ブラウザが対応しているHTTPバージョン1.1では、Cache-Controlがキャッシュ制御用のヘッダーフィールドとして利用されている。
Last-Modifiedヘッダーは、Cache-Controlでno-cache以外が指定されてキャッシュが利用されるシーンにおいて、有効期限の判別材料として使われるらしい。。RFCではキャッシュの有効性チェック関連で大まかな規定はされているようだが、細かい部分は個々のプログラムに依ってしまうようだ。( http://www.studyinghttp.net/caching )
以上の調査より、以下の結論を導き出せる。
「ページの有効期限切れ」画面が・・・
つまるところ件の画面はIEのバグでも何でもなく、RFCに従った正常な動作であることが導き出せた。Firefoxの場合も、キャッシュがないため再度リクエストを送る動作なのでRFCから逸脱していない。ではいったいなぜ、標準に従っている筈のこの動作について多くのWebプログラマーが頭を悩ませるのか?また、巷間にあふれる処方箋はどこまでが正しいのか?
以下では、この二つの疑問について「そもそも論」と「PHPのソースコード」の二つの側面から追求し、もっとも効果的かつ抜本的な対策案「チケットの導入」を考える。
まずこの点について考えなければならない。なぜなら"「ページの有効期限切れ」画面を表示させたくない"という要求自体が無くなれば、そもそもこの対策に頭を悩ませる必要はなくなる。
ユーザーの立場に立てばいずれも至極まっとうな要求である。しかしWebプログラマはこれらの要求を聞くと眉をひそめる。なぜか?
つまり以下の二点が頭を悩ませる最大の要因である。
これが為に本来であれば意味的に至極まっとうな、IEの「ページの有効期限切れ」画面やFirefoxの「再POSTの確認」ダイアログを、ユーザーを戸惑わせたくないが為にどうにかして無効化する必要が出てくるのであろう。元々入力値や個人情報の安全性を補助するためのキャッシュ無効化機能が、そのクライアントプログラム側のインターフェイス故に、結果としてユーザーを戸惑わせる機能として否定されようとしているのだ。Webプログラマは、結果としてキャッシュ機能を有効化することにより元の入力フォームに"戻らせ"、代わりに再POST防止のため頭をひねることとなる。
ではいったい、キャッシュ機能を正しく有効にするにはどうすればよいのか?PHPに限定して、その処方箋をざっとGoogleで検索してみた結果を以下に列挙する。
どうやらsession_cache_limiter()に結局は落ち着くようである。関連するPHPマニュアルを以下に列挙する。
PHPソースコードの探索に入る。ソースコードのバージョンは4.3.11, ターゲットは ext/session/session.c のみである。
まずPHP関数を定義している"PHP_FUNCTION"マクロを使用した関数の中からsession_cache_limiter, session_cache_expireを探る。1331行目以降に出てくる。以下、適当に省略して要点のみを抜き出す。
// 現在のcache_limiterを返す。引数が渡されれば、引数でcache_limiterを更新する。 PHP_FUNCTION(session_cache_limiter) { zval **p_cache_limiter; char *old; zend_get_parameters_ex(ac, &p_cache_limiter); old = estrdup(PS(cache_limiter)); convert_to_string_ex(p_cache_limiter); zend_alter_ini_entry("session.cache_limiter", sizeof("session.cache_limiter"), Z_STRVAL_PP(p_cache_limiter), ...); RETVAL_STRING(old, 0); } // 現在のcache_expireを返す。引数が渡されれば、引数でcache_expireを更新する。 PHP_FUNCTION(session_cache_expire) { zval **p_cache_expire; long old; old = PS(cache_expire); zend_get_parameters_ex(ac, &p_cache_expire); convert_to_long_ex(p_cache_expire); PS(cache_expire) = Z_LVAL_PP(p_cache_expire); RETVAL_LONG(old); }
どうやらPS(cache_limiter), PS(cache_expire), session.cache_limiter辺りが怪しいようです。ただ、なぜcache_limiterではiniエントリを更新していてexpireの方ではそれをしないかは不明です。
147行目でこの二つがiniファイルより初期化されています。
STD_PHP_INI_ENTRY("session.cache_limiter", "nocache", PHP_INI_ALL, OnUpdateString, cache_limiter, ...) STD_PHP_INI_ENTRY("session.cache_expire", "180", PHP_INI_ALL, OnUpdateInt, cache_expire, ...)
このマクロの詳細は不明ですが、デフォルト値とおぼしき値はマニュアルと一致しています。
PS(cache_limiter)は前述のPHP_FUNCTION(session_cache_limiter)および822行から始まる
static int php_session_cache_limiter(TSRMLS_D)
内でしか使われていません。
PS(cache_expire)は770行目
CACHE_LIMITER_FUNC(public)
および789行目
CACHE_LIMITER_FUNC(private_no_expire)
の二カ所で使われています。
ざっとみてだいぶ本命に近づいてきたようです。cache_expireは秒数として使われているようです。cache_limiterを探ると本命に近づけそうです。
PS(cache_limiter)が使われているphp_session_cache_limiter()について探索してみます。
// 238行目:session_cache_limiter()の各引数と対応する関数のマッピング用構造体の定義 typedef struct { char *name; void (*func)(TSRMLS_D); } php_session_cache_limiter_t; // 814行目:実際の構造体の実体 static php_session_cache_limiter_t php_session_cache_limiters[] = { CACHE_LIMITER_ENTRY(public) CACHE_LIMITER_ENTRY(private) CACHE_LIMITER_ENTRY(private_no_expire) CACHE_LIMITER_ENTRY(nocache) {0} }; // 822行目:php_session_cache_limiter()の要点のみ抜粋 static int php_session_cache_limiter(TSRMLS_D) { php_session_cache_limiter_t *lim; // 構造体の中から、現在のcache_limiterに一致するのを取り出し、対応付けされている関数を実行する。 for (lim = php_session_cache_limiters; lim->name; lim++) { if (!strcasecmp(lim->name, PS(cache_limiter))) { lim->func(TSRMLS_C); return 0; } } return -1; }
php_session_cache_limiter()自体はsession_start()内で php_session_reset_id(TSRMLS_C) の後に呼ばれています(1097行目)。
確信まであと少しです。構造体に使われているマクロで注目してみると、構造体の実体のすぐ上にCACHE_LIMITER_FUNCと言う一連の関数が定義されていました。ヘッダーフィールドらしき文字列も見えます。
この一連の関数こそが、Expires/Pragma/Cache-Control/Last-Modifierをセッション使用時に制御している中枢です。実体は751行目のlast_modifiedから始まり813行目まで続きますが、要はどの関数がどのヘッダーフィールドを送信しているのか分かればよいので、そこだけまとめました。
ようやく追いつめました。 これらが、session_cache_limiter()により呼び出されるキャッシュ制御ヘッダーフィールドを送出している部分です。これらはphp_session_cache_limiter()内で、現在のcache_limiterから呼び出されます。
Webを調べていると、session_cache_limiter()について次の例がそれぞれ別の場所で見つかった。
session_cache_limiter('none'); session_cache_limiter('no-cache'); session_cache_limiter('private, must-revalidate');
これまでの調査によると、これらの引数は明らかに無効である。しかし掲載場所ではいずれも「これで思った通りに件の画面が表示されなくなった。」と報告されていた。
この理由は簡単で、php_session_cache_limiter()では引数(つまり現在のcache_limiter)にヒットしなかった場合はそのままスルーしている。スルーするとどうなるか。単にキャッシュ制御ヘッダーフィールドが送出されなくなるだけである。そうなると、只のHTMLと同様に普通にブラウザにキャッシュされ、結果、「ページの有効期限切れ」画面はブラウザバックでは表示されなくなる。
これが今まで明らかにされていなかった理由としては、session_cache_limiter()に指定した値が有効かどうか判別するためのインターフェイスが無いことが考えられる。それに加え、関数名からCache-Controlヘッダーに指定するものを渡せばよいと誤解を招いた可能性もある。前掲の2・3番目の間違いがそれにあたると思われる。
以上より、今回の件に関する動作はほぼ押さえられた。結論としては「ページの有効期限切れ」画面を表示させない対策としてはsession_cache_limiter('private_no_expire')をコールすることが最大公約数として導き出せる。(*3)
しかし、再POSTの危険性に対してはどうすればよいのか?多くが試行錯誤中ではあり、たとえばDBにレコードを挿入・更新する場面ではUNIQUE違反を検出する場合がある。しかし、この場合UNIQUEキーが無く単純にシーケンスと関連づけされたPrimary Keyだけが有効となる場面ではUNIQUEを検出できない。
一つの手法としては、都度ユニークなキー値を生成し、hiddenとしてフォームに埋め込んでおく「チケット」の導入が考えられる。アプリケーションは裏側でチケットを管理し、アクションが発生すればチケットに「使用済み」マークを付ける。もしも使用済みマークのついたチケットがPOSTされてきたら、エラーで弾く。Javaでの開発では導入される場合があるようだが、これが比較的安全であろう。
あるいは、そもそも個人情報をキャッシュさせないためのキャッシュ制御であるのだから、ユーザーに「そういうもの」として受け止めて頂くよう説得し、「ページの有効期限切れ」画面が表示されることに対して理解を求める、という手もある。
ヘルプに入っているプロジェクトで、妙な現象に悩まされた。
JavaのWebアプリで、同じホストにOC4Jで二つのWebアプリを動かしている。
で、一日ほど頭を冷やして冷静にGoogleで検索したら一発でヒットしてすごい哀しい(*1)。
・The cookie may be lost when a window is opened from a modal or modeless HTML dialog box in Internet Explorer 6
http://support.microsoft.com/kb/831678/en-us
「IEの仕様」であることは確かだが、回避方法も上記KnowledgeBaseに載っている。
まずshowModalDialogをするときに第二引数に元のアプリのwindowオブジェクトを渡す。さらにpopupウインドウ内からdialogArguments経由で取得されたwindowオブジェクトを使って、open()でウインドウを開く。
KBに掲載されているのはASPコードだが、お好みの言語で簡単に確認できる。自分の場合はPHPコードで簡単に確認できた。確かに、showModalDialogに渡されたwindowオブジェクトのopen()であれば、元ウインドウのCookieが正常に引き継がれた。
PHPサンプルコードのzip : 添付ファイル/2008/12/13/021722/kb831678_php_sample.zip
最近個人的に好きになった if ブロックのコメントの付け方。
if (condition_1_is_true) { // condition_1 is true ... } else if (condition_2_is_true) { // condition_2 is true ... }
実は今まで、下のようなコメントを書く時もあったのですが・・・
// condition_1 is true if (condition_1_is_true) { ... }
これだと、下にelse ifなどのブロックを付け足す時、付け足すブロックのコメントをどこに書けば良いのか困る時があるわけです。
というわけで、ifブロックでコメントを付ける時・・・
というような感じになるように最近は気をつけてます。
// ほげほげを分岐するブロックです:ifブロック全体の内容 if (cond_1_is_true) { // cond_1 が真の時:個別のifブロックの内容 ... } else if (cond_2_is_true) { // cond_2 が真の時:個別のifブロックの内容 ... } ...
ITプロジェクトとかで、@ITとか見てるとしょっちゅう「敏腕プロジェクトマネージャがいない」という嘆きを目にするんだけど・・・。
すごい大雑把な私感なんだけど、敏腕PMが「いなくても」何とかなるようなやり方って無いものかなぁと。
実際問題、デスマったりなんだりかんだりしつつも世の中動いているというのは須く「人間」が働いている故のたまものだよなと。
それだけでも充分すごいと思うので、敏腕PMとか人間的にスーパーマンなPMとかがいなくても、定時帰りできるような上手い仕事のやり方というか回し方って無いもんかなぁと。
以上、非常に大雑把な私感。