home ホーム search 検索 -  login ログイン  | help ヘルプ

find 検索

1301 - 1310 / 1320    [|<]  [|<]  [<]  131  132  [>]  [>|][>|]
タイトル/名前 更新者 更新日
技術/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
ソート項目 / ソート順     1ページ 件ずつ表示

技術/CVS  

所有者: msakamoto-sf    作成日: 2007-07-08 00:25:03
カテゴリ: CVS 

CVSメモ。主にCVSを使っていて、嵌ってしまったところや一般的な使用方法の個人用メモなど。

CVS自体の使い方は、下記が今のところ一番安定しているドキュメントか?

http://www.linkclub.or.jp/~tumibito/soft-an/cvs/cvs-man/


CVSで嵌りやすいところ

バージョン管理システムと言えば、直観的で分かりやすい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 というのにもかなり振り回された。

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

とかやられた日には、何がなんだか分からなくなる・・・というか、分からなかった。

SSH越しで嵌る!

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側で接続したときに正体不明のプロンプトらしき文字列に悩まされることはない。

CVSの使い方

様々な書籍やWeb上のリンクが豊富にある為、基本的な利用法は特に書かない。

setenv.sh

リポジトリの位置はあくまでもCVSROOT環境変数ので決まる。これ、.bash_profileとかに書くと、複数のリポジトリを併用する場合煩わしい。

他のシーンでも自分がよく使っている手として、setenv.shというのを作成しておき、切り替えて使う、というのをメモしておく。以下の様なsetenv.shを、ローカルにcheck outしたディレクトリの一つ上とかに作成しておき、切り替えられるようにしておく。あるいは setenv_(project名).shとかにしておいても良い。

  • setenv.sh (bash用)
#!/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

とすれば良い。

*1: 大学4年
*2: cvs initなどを除く

プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-16 00:28:29
md5:0c5612b18f51edae58bfe7123d58b303
sha1:75da3fe76be1aafa265e9701ee207ca1e46502bb

PHP/「ページの有効期限切れ」対策  

所有者: msakamoto-sf    作成日: 2005-07-10 23:28:13
カテゴリ: PHP Web 

特にIE系でよく見られる「ページの期限切れ」画面。これを発生させないためにはどうすればよいのか、現在は少しGoogleで検索するだけで実に様々な対策方法が蓄積されている。だが、そもそも「ページの期限切れ」とはいったい何を示しているのか?いったいこの画面はユーザーに何を訴えているのだろうか?

今回はPHP言語に限定して、この現象を可能な範囲その原因を追及し、抜本的対策と巷間にあふれる対策方法の是非を検討する。最終的に必要となった知識はHTTPのRFC2616のキャッシュ機能およびPHPのext/session/session.cのソースコードとなった。

「ページの有効期限切れ」が発生するスクリプト・発生しないスクリプト

まず現象を再現するところから始める。「ページの有効期限切れ」画面が表示されるのはIE系なので、以下に示す各スクリプトもIEで表示させることを前提とする。Firefoxの場合の動作は後述する。

発生方法

  1. 「発生するスクリプト」を設置し、IE系のブラウザでアクセスする。
  2. Submitボタンをクリックし、フォームを送信する。(テキストボックスは単に見栄えや何かの問題で付けただけで、特に使用することはないので適当な値を入力しても、空のまま送信しても問題ない)
  3. 3~4回Submitしたのち、ブラウザバックする。(「戻る」ボタン or BackSpace)
  4. 「ページの有効期限切れ」画面が表示される。

発生するスクリプト

  • session01.php
<?php
session_name('CacheExpireExperiment01');
session_start();
?>
<html>
<body>
<form action="" method="POST">
<input type="text" value="" name="">
<input type="submit">
</form>
</body>
</html>
  • session02.php
<?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>

発生しないスクリプト

  • session03.php
<?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でも発生しない。)form01.html
<html>
<body>
<form action="" method="POST">
<input type="text" value="">
<input type="submit">
</form>
</body>
</html>

ブラウザで異なる挙動(IE/FireFox)

発生するスクリプトの場合(Firefox)

「ページの有効期限切れ」画面は主にIE系で取り上げられる現象である。一方のNetscape系(Geckoエンジン系)のブラウザではあまりそう言った話題はあがらない。では、実際どうなるのか?Firefox1.04で前掲のsession01/02.phpにアクセスし、IEの時と同様の手順を踏んでみる。

結論として、「再度POSTして良いですか?」という下図に示すような画面が表示され、OKをクリックすると再度サーバーにリクエストが送られている。(この段階ではあくまでも送られている「らしい」までしか目視確認できないが、後述のFirefoxのLiveHTTPHeadersの解析により実際にリクエストが送られていることを確認できた。)

画像/PHP/re_post_confirm.jpg

発生しないスクリプトの場合(IE/Firefox)

IE/Firefoxとも、発生しないスクリプトの場合特にサーバーにアクセスも発生せず、表示の早さとフォームに入力された値を覚えてくれている辺り(実際、各Submit毎に入力した値を完全に覚えていた)も同様である。どうやらブラウザのキャッシュにアクセスしているらしい。

推測:session_cache_limiter()が原因か?

発生するスクリプト・しないスクリプトの違いはsession_cache_limiter()の違いである。発生するスクリプトはsession_cache_limiter()を呼んでいないか、'nocache'を渡している。実際にPHPのマニュアルを参照してみる。

http://jp.php.net/manual/ja/function.session-cache-limiter.php

これによると、この関数はHTTPヘッダーを操作し、クライアントに対してキャッシュ制御を行う関数らしい。引数はマニュアルを読む限り次の4つ。

nocache
クライアント(および途中に介在するプロクシ)にキャッシュさせない。
public
クライアント(および途中に介在するプロクシ)にキャッシュを許す。
private
クライアントにのみキャッシュを許す。途中に介在するプロクシにはキャッシュを許さない。
private_no_expire
機能はprivateと同じだが、Mozilla系を混乱させないためExpiresヘッダを送信しない。

当関数を呼ばない場合、自動的にsession.cache_limiterに指定された値が適用されるらしい。そのデフォルト値は'nocache'であるらしい。従って、前掲のsession01.phpは当関数を呼んでいないため'nocache'が仮定され、結果として動作はsession02.phpと同等のものになっていたことが推測される。

HTTPヘッダーの比較

session_cache_limiter()はHTTPヘッダーを操作する。従って、前掲のスクリプトの動作を解析するにはそのHTTPヘッダーを観察する必要がある。今回はFirefoxの拡張(extension)の一つであるLiveHTTPHeadersを用いて、前掲の三つのスクリプトにアクセスした場合のHTTPヘッダーを観察してみることにした。

session01 - 03.phpにアクセスする。いずれもアクセスの前にCookieとブラウザキャッシュを全クリアする。操作としては以下の手順で統一した。

  1. アドレス欄に直接URLを入力し、GETでアクセスする。(GET一回目)
  2. フォームに適当な値を入力し、Submitする。(POST一回目)
  3. フォームに前回とは異なる値を入力し、再度Submitする。(POST二回目)
  4. ブラウザバックする。(IEなら「ページの有効期限切れ」画面が表示されるが、Firefoxの場合は再度POSTされる。)

このときのLiveHTTPHeadersのヘッダーを以下のファイルに保存した。

以下にログファイルを解析した結果を、要点を絞ってまとめる。

  • session01.phpのサーバーからの応答ヘッダー

サーバーからの応答の内、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
  • session02.phpのサーバーからの応答ヘッダー
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
  • session03.phpのサーバーからの応答ヘッダー
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のキャッシュ制御について

HTTPについての文献は次のURLに日本語で非常に詳しくまとめられている。以下、このHPを元に今回の調査対象となるヘッダーフィールドおよびブラウザのキャッシュ制御について著者の視点からまとめてみる。

注意 :日本語訳の使用上の注意点については必ず http://www.studyinghttp.net/translations#Notice を参照して下さい。あくまでも英語文書が正式版であり、日本語訳した場合の誤訳や誤解については無保証だそうです。

キャッシュされる場所とキャッシュの許可の考え方

Webページがキャッシュされる場所はクライアントという視点では二カ所ある。

  • クライアントPCのWebブラウザ(つまりクライアントPC)
  • プロクシサーバー

プロクシサーバーは一般に共用サーバーである。この事により、「個人のプライバシーに関わるページのキャッシュの是非」を操作できる必要性が生じる。これにより、キャッシュには主に以下の3段階の"レベル"が導入されるべきである。

  1. クライアントPCおよびプロクシサーバーにキャッシュされても大丈夫な"公開(public)"レベル
  2. クライアントPCにのみキャッシュされ、プロクシサーバーにはキャッシュされるべきではない、"個人(private)"レベル
  3. クライアントPC/プロクシサーバーの両方にキャッシュを許さない、"キャッシュ不可(no-cache)"レベル

さらに重要な点として、「キャッシュが無効化されるのをいつ、どのようにしてブラウザは認識するのか」という問題も出てくる。すなわち、

  1. キャッシュレベル
  2. キャッシュ無効化検出

の二点を制御できて初めてキャッシュを安全に確実に利用できるようになる。

HTTP/1.0におけるキャッシュ制御ヘッダーフィールド(Expires, Pragma)

現在主なサーバー・ブラウザ(クライアントプログラム)が対応しているHTTPのバージョンは 1.1/1.0 の二種類。このうち、HTTP/1.0時代のキャッシュ制御ヘッダーフィールドについてまとめる。

Expires
そのWebページ(リソース)がいつ無効になるのかを示す。形式は RFC 1123( http://www.studyinghttp.net/header#HTTP-Date 参照) にて定義*1されている。
Pragma
本来は汎用に使えるOptionalなヘッダーフィールドを目的としていたが、実際にはキャッシュを許可しない*2"no-cache"のみが定義されている。

Expires, Pragma ともにHTTP/1.0時代のフィールドだが、下位互換性のため使用している。HTTP/1.1に対応したプログラムの場合はCache-Controlヘッダーフィールドがこれらより優先して使用される。

HTTP/1.1におけるキャッシュ制御ヘッダーフィールド(Cache-Control, Last-Modified)

近年の主要なサーバー・ブラウザが対応しているHTTPバージョン1.1では、Cache-Controlがキャッシュ制御用のヘッダーフィールドとして利用されている。

Cache-Control
次のURLを参照のこと。 http://www.studyinghttp.net/cgi-bin/rfc.cgi?2616#Sec14.9
Last-Modified
一般的にメッセージの最終更新日を表す。ファイルアクセスであればそのサーバーのファイルシステムにおける最終更新日を返すべきであるし、CGIなどによるなにがしかのメッセージであれば、そのメッセージが生成された(すなわちそのCGIが起動・生成した)日付を返すべきである。

Last-Modifiedヘッダーは、Cache-Controlでno-cache以外が指定されてキャッシュが利用されるシーンにおいて、有効期限の判別材料として使われるらしい。。RFCではキャッシュの有効性チェック関連で大まかな規定はされているようだが、細かい部分は個々のプログラムに依ってしまうようだ。( http://www.studyinghttp.net/caching

ヘッダーフィールドと「ページの有効期限切れ」画面の関連についての第一のまとめ

以上の調査より、以下の結論を導き出せる。

「ページの有効期限切れ」画面が・・・

  1. 表示される場合とは、すなわちHTTPヘッダーのキャッシュ制御フィールドにおいてキャッシュ不可を指示された場合である。
  2. 表示されない場合とは、すなわちHTTPヘッダーのキャッシュ制御フィールドにおいてキャッシュ可能である事を指示された場合である。

つまるところ件の画面はIEのバグでも何でもなく、RFCに従った正常な動作であることが導き出せた。Firefoxの場合も、キャッシュがないため再度リクエストを送る動作なのでRFCから逸脱していない。ではいったいなぜ、標準に従っている筈のこの動作について多くのWebプログラマーが頭を悩ませるのか?また、巷間にあふれる処方箋はどこまでが正しいのか?

以下では、この二つの疑問について「そもそも論」と「PHPのソースコード」の二つの側面から追求し、もっとも効果的かつ抜本的な対策案「チケットの導入」を考える。

そもそもなぜ「ページの有効期限切れ」対策に頭を悩ませるのか?

まずこの点について考えなければならない。なぜなら"「ページの有効期限切れ」画面を表示させたくない"という要求自体が無くなれば、そもそもこの対策に頭を悩ませる必要はなくなる。

  • なぜ件の画面が表示されると都合が悪いのか?
    • 格好と見栄えが悪い。
    • 意味不明の画面。何か異状があったのかと戸惑う。
  • では、なぜ件の画面が表示されるのか?
    • ユーザーがブラウザバックを実行する。
    • なぜブラウザバックを実行するのか?
      • (確認画面などで確認した結果)フォームに入力した値が間違っていたので、''前の画面に戻って''修正したい。
      • フォームに入力したが、続く手続きをキャンセルしたい。
  • ではユーザーはブラウザバックをしたとき何が表示されていることを望むのか?
    • 直前の入力画面
    • 当然、自分が入力した値が表示されているべき。

ユーザーの立場に立てばいずれも至極まっとうな要求である。しかしWebプログラマはこれらの要求を聞くと眉をひそめる。なぜか?

  • ブラウザバックされた後に再度POSTされるとまずい。
  • なぜまずいのか?
    • 登録した後にブラウザバックされてPOSTされると、同じ情報がPOSTされてエラーとなる。→プログラマーとして気持ち悪い。

つまり以下の二点が頭を悩ませる最大の要因である。

  • 本来は再度POSTされると面倒なので、キャッシュを不可にしてブラウザバックを無効化したい。
  • ユーザーとしては直感的に「データの修正」のためにブラウザバックを実行してしまい、またブラウザバックができて当然だと考えている。

これが為に本来であれば意味的に至極まっとうな、IEの「ページの有効期限切れ」画面やFirefoxの「再POSTの確認」ダイアログを、ユーザーを戸惑わせたくないが為にどうにかして無効化する必要が出てくるのであろう。元々入力値や個人情報の安全性を補助するためのキャッシュ無効化機能が、そのクライアントプログラム側のインターフェイス故に、結果としてユーザーを戸惑わせる機能として否定されようとしているのだ。Webプログラマは、結果としてキャッシュ機能を有効化することにより元の入力フォームに"戻らせ"、代わりに再POST防止のため頭をひねることとなる。

PHPのソースコードの探索

ではいったい、キャッシュ機能を正しく有効にするにはどうすればよいのか?PHPに限定して、その処方箋をざっとGoogleで検索してみた結果を以下に列挙する。

どうやらsession_cache_limiter()に結局は落ち着くようである。関連するPHPマニュアルを以下に列挙する。

PHPソースコードの探索に入る。ソースコードのバージョンは4.3.11, ターゲットは ext/session/session.c のみである。

PHP_FUNCTION(session_cache_limiter), PHP_FUNCTION(session_cache_expire)

まず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の方ではそれをしないかは不明です。

PS(cache_limiter), PS(cache_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を探ると本命に近づけそうです。

php_session_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と言う一連の関数が定義されていました。ヘッダーフィールドらしき文字列も見えます。

CACHE_LIMITER_FUNC(public, private_no_expire, private, nocache)

この一連の関数こそが、Expires/Pragma/Cache-Control/Last-Modifierをセッション使用時に制御している中枢です。実体は751行目のlast_modifiedから始まり813行目まで続きますが、要はどの関数がどのヘッダーフィールドを送信しているのか分かればよいので、そこだけまとめました。

  • CACHE_LIMITER_FUNC(public)
    • Expires: (PS(cache_expire)などを吟味して送出)
    • Cache-Control: public, max-age=PS(cache_expire)*60
    • Last-Modified: (last_modified関数)
  • CACHE_LIMITER_FUNC(private_no_expire)
    • Cache-Control: private, max-age=PS(cache_expire)*60, pre-check=PS(cache_expire)*60
    • Last-Modified: (last_modified関数)
  • CACHE_LIMITER_FUNC(private)
    • Expires: Thu, 19 Nov 1981 08:52:00 GMT (固定)
    • Cache-Control: (private_no_cache呼び出し)
    • Last-Modified: (private_no_cache呼び出し)
  • CACHE_LIMITER_FUNC(nocache) (全て固定)
    • 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

ようやく追いつめました。 これらが、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での開発では導入される場合があるようだが、これが比較的安全であろう。

あるいは、そもそも個人情報をキャッシュさせないためのキャッシュ制御であるのだから、ユーザーに「そういうもの」として受け止めて頂くよう説得し、「ページの有効期限切れ」画面が表示されることに対して理解を求める、という手もある。


*1: PHPのsession.cのソース内では gmdate("D, d M Y H:i:s") として生成されている。
*2: 正確には、たとえクライアントプログラムがキャッシュを保持していたとしても、それを無視して再度リクエストを送出することをRequestする。
*3: 'public'だとプロクシに残るCache-Controlになるので個人情報保護の観点からN.G.となる。'private'ではMozilla系が混乱するという情報があり、残った'private_no_expire'が公約数として使用できる。無効な文字列を指定してそもそもヘッダーを送出させないのは、全てを分かった上での反則技としてなら有効。

プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-16 00:03:24
md5:8302f7f73bf29530d2fd90de8461e9b8
sha1:1fdd8b3e80a81d98ec8c245452a3f0c98665a666

添付ファイル/PHP/session03.log  

所有者: msakamoto-sf    作成日: 2005-07-10 00:02:24
カテゴリ: PHP 添付ファイル 
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-16 00:02:42
md5:2125cbc2e46edec827a1c8ed1474764c
sha1:a367ab59de433078069882247c975d4145e1c210

添付ファイル/PHP/session02.log  

所有者: msakamoto-sf    作成日: 2005-07-10 00:01:55
カテゴリ: PHP 添付ファイル 
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-16 00:02:15
md5:0169d93a624d33cd39a980b1705f458b
sha1:e614e54fe35c70400089c80a28c8f684d26a603d

添付ファイル/PHP/session01.log  

所有者: msakamoto-sf    作成日: 2005-07-10 00:01:05
カテゴリ: PHP 添付ファイル 
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-16 00:01:34
md5:3baa2b9286daf1ac50edb47181e634c6
sha1:8a961dd0f77d12301e46b78a83faaf4ac30d1cee

画像/PHP/re_post_confirm.jpg  

所有者: msakamoto-sf    作成日: 2005-07-10 23:59:14
カテゴリ: PHP 画像 
画像/PHP/re_post_confirm.jpg
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-16 00:00:25
md5:f8bd84e09e53a18505a11eab78466ef1
sha1:2742a057621477ef247e04dc52cc8f4e1ab287ed

日記/2008/12/13/IEのshowModalDialog内からの遷移でCookieが渡らない件  

所有者: msakamoto-sf    作成日: 2008-12-13 02:01:51
カテゴリ: PHP Web Windows プログラミング 

ヘルプに入っているプロジェクトで、妙な現象に悩まされた。
JavaのWebアプリで、同じホストにOC4Jで二つのWebアプリを動かしている。

  1. IE6/7を立ち上げて、アプリ1にログインする。
  2. もう一つIE6/7のウインドウを立ち上げ、アプリ2にログインする。
  3. アプリ2でwindow.showModalDialog()を使ってポップアップウインドウを開く。
  4. 上記ポップアップウインドウ内からtarget="_blank"のアプリ2のURLリンクを開く。
  5. →このとき、target="_blank"で開かれたウインドウに、アプリ2のCookieが渡らない。

で、一日ほど頭を冷やして冷静に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

*1: ヤバイよ、お客に"IEの仕様らしく対応を直ぐには取れないので、ブラウザは1つだけ立ち上げるようにして下さい"って言っちゃったよ。恥ずかしい・・・

プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-13 02:19:40
md5:1c57ec746b5eb2123c00f28a83d0ca1e
sha1:cf679a79f28e2ff4216c6c595d829cf9a21029f9

添付ファイル/2008/12/13/021722/kb831678_php_sample.zip  

所有者: msakamoto-sf    作成日: 2008-12-13 02:17:22
カテゴリ: PHP Web Windows プログラミング 添付ファイル 
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-13 02:17:59
md5:66f5aa4660a2ee4bf3f14ab2115f4a88
sha1:feabd901d5191fe646938a8de01efa571af69e24

日記/2008/12/04/ifブロックのコメントの付け方  

所有者: msakamoto-sf    作成日: 2008-12-04 00:27:49
カテゴリ: プログラミング 

最近個人的に好きになった 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の前に書いておく。
  • 個々のifブロックの条件の内容をコメントする時は、個別のifブロックの中に書いておく。

というような感じになるように最近は気をつけてます。

// ほげほげを分岐するブロックです:ifブロック全体の内容
if (cond_1_is_true) {
    // cond_1 が真の時:個別のifブロックの内容
    ...
} else if (cond_2_is_true) {
    // cond_2 が真の時:個別のifブロックの内容
    ...
}
...

プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-12-04 00:32:37
md5:cb3a3c71eabf67fc51fca4f6898b472c
sha1:ad914590a9d16ad1b9788d9d3be0a30c08d8a09d

日記/2008/11/30/「プロジェクトマネージメント」についての私感  

所有者: msakamoto-sf    作成日: 2008-11-30 10:21:03
カテゴリ:

ITプロジェクトとかで、@ITとか見てるとしょっちゅう「敏腕プロジェクトマネージャがいない」という嘆きを目にするんだけど・・・。
すごい大雑把な私感なんだけど、敏腕PMが「いなくても」何とかなるようなやり方って無いものかなぁと。
実際問題、デスマったりなんだりかんだりしつつも世の中動いているというのは須く「人間」が働いている故のたまものだよなと。
それだけでも充分すごいと思うので、敏腕PMとか人間的にスーパーマンなPMとかがいなくても、定時帰りできるような上手い仕事のやり方というか回し方って無いもんかなぁと。

以上、非常に大雑把な私感。


プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2008-11-30 10:26:28
md5:f5b29017dd772e6cefd6f04b6fb6c53e
sha1:7d087b5fba537626c497bc99e4d5741a4375ab18