PHPプログラムパーツ

Copyright(C) 25Feb2014 coskx TNCT

はじめに

ここではPHPプログラム上の基本部品サンプルを紹介する。すべてのファイルは,htdocs内にあり,http://・・・
としてブラウザから閲覧するものとする。

  1. ダイジェスト認証(MD5を使って暗号化してユーザID,パスワードを認証)
  2. WWWサーバからクライアントPCへのファイルのダウンロード
  3. クライアントPCからWWWサーバへファイルのアップロード
    3.1 単一ファイルのアップロード
    3.2 複数ファイルのアップロード
    3.3 jQueryを用いたD&D複数ファイルのアップロード
  4. WWWサーバでの排他的ファイル書き出し
  5. クッキーの利用
  6. セッションの利用



1.ダイジェスト認証


ダイジェスト認証とは,MD5を使って暗号化してユーザID,パスワードを認証し,
ページ内に入れるようにする方法である。一度認証されると,ブラウザを閉じるまで認証状態になる。
次のサンプルPHPが呼び出されると,ユーザIDとパスワードを求められる。
プログラム中で設定されたユーザIDとパスワードを正しく入力すると,
関数authentification()内でexitせずに通過することができて,認証は成功する。
なお,PHPプログラムでは,関数定義は場所を気にせずにおくことができ,関数定義を除いた部分で
プログラムの開始場所が定まる。

このサンプルでは,ユーザID:TokyoTaro,パスワード:himitsudesu,で認証が成功する。

list 1.1 ダイジェスト認証PHPサンプル1
<?php
function authentification($A1,$realm) /*Digest HTTP 認証*/
{
    $auth = false;

    $err = "<html><body><br><br><center>\n"
    . "<h2>認証が失敗したか,認証がキャンセルされました</h2>\n"
    . "<h2>本ページの表示にはユーザーの正しい認証が必要です</h2>\n"
    . "<br><br></center></body></html>\n";

    if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
        $data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST']);
        //$receivedUserID=trim($data['username']);             //ユーザID
        $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']);
        $valid_response = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);
        if ($data['response'] == $valid_response) $auth = true;
    }
    if ($auth === false) {
        header('HTTP/1.0 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",nonce="' . md5(uniqid()) . '",opaque="' . md5($realm). '"');
        echo $err;
        exit; //認証失敗
    }
}

// http auth ヘッダをパースする関数
//http://php.net/manual/ja/features.http-auth.phpより
function http_digest_parse($txt)
{
    // データが失われている場合への対応
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));
    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);
    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }
    return $needed_parts ? false : $data;
}

function makeA1($userID,$pass,$realm)
{
    return md5($userID . ':' . $realm . ':' . $pass);
}

//////////   ここからphpメイン   ///////////////////////

$userID="TokyoTaro";
$password="himitsudesu";
$realm = 'Restricted area';
$A1=makeA1($userID,$password,$realm);
//本当は$A1はあらかじめ求められて,パスワードファイルなどに保存されている。

authentification($A1,$realm);
//関数authentification()内でexitせずに出てこられたら認証成功
?>

<html>
<body>
<?php
print "<h1>認証されました</h1>\n";
?>
</body>
</html>


認証動作は,1回のアクセスのように見えるが,実は次のように2回のアクセスからなる。
(1)ブラウザはhttp://・・・でこのページにアクセスする。
(2)このページの閲覧をブラウザが要求してきた時は,「ここからphpメイン」から始まって,関数authentification()を呼び出す。
まだユーザIDもパスワードももらっていないので,authentification()内では isset($_SERVER['PHP_AUTH_DIGEST'])がfalseとなり,$authはfalseのままであって,if ($auth === false) {に入り,'HTTP/1.0 401 Unauthorized'と'WWW-Authenticate:・・・'をブラウザに返す。
(3)ブラウザはこれらを受け取ると,ユーザIDとパスワードを求めるダイアログを表示し,それらを受け取り,ユーザIDとパスワードをhash関数で暗号化してもう一度このページにアクセスする。
(4)暗号化されたユーザIDとパスワードを受け取るので,今度はisset($_SERVER['PHP_AUTH_DIGEST'])がtrueになり,受け取った文字列でユーザIDとパスワードの認証を行う。


PHPの関数についての補足説明

PHPで関数は次のように記述される。xxxxxは関数の名前である。$x1,$x2は引数である。
function xxxxx($x1,$x2)
{
    関数の中身
    return $z; //必要に応じて値を持ち帰る
}
関数はPHP記述内ならどこに書いても良い。
function で始まるので,実行文では無いことがわかるからである。


複数ユーザがいて,ユーザIDによってパスワードをファイルなどから調べて一致するかどうかというプログラムを作成するなら,次のサンプルのほうが有効である。
次のサンプルでも,ユーザID:TokyoTaro,パスワード:himitsudesu,で認証が成功する。


list 1.2 ダイジェスト認証PHPサンプル2
<?php
function authentification($realm) /*Digest HTTP 認証*/
{
    $auth = false;

    $err = "<html><body><br><br><center>\n"
    . "<h2>認証が失敗したか,認証がキャンセルされました</h2>\n"
    . "<h2>本ページの表示にはユーザーの正しい認証が必要です</h2>\n"
    . "<br><br></center></body></html>\n";

    if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
        $data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST']);
        $receivedUserID=trim($data['username']);             //ユーザID
        $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']);
        $A1=getPASSWD_A1($receivedUserID);
        $valid_response = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);
        if ($data['response'] == $valid_response) $auth = true;
    }
    if ($auth === false) {
        header('HTTP/1.0 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="' . $realm . '",qop="auth",nonce="' . md5(uniqid()) . '",opaque="' . md5($realm). '"');
        echo $err;
        exit; //認証失敗
    }
    return $receivedUserID;
}

// http auth ヘッダをパースする関数
//http://php.net/manual/ja/features.http-auth.phpより
function http_digest_parse($txt)
{
    // データが失われている場合への対応
    $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
    $data = array();
    $keys = implode('|', array_keys($needed_parts));
    preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);
    foreach ($matches as $m) {
        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
        unset($needed_parts[$m[1]]);
    }
    return $needed_parts ? false : $data;
}

function getPASSWD_A1($userID)
//この関数内でファイルから対応するパスワードを読み込んだりデータベースから取り込んだりすればよい。
{
    global $mydatabase;
    $error="error";
    if ($userID==$mydatabase['userID']) return $mydatabase['A1'];
    else return $error;
}

function makeA1($userID,$pass,$realm)
{
    return md5($userID . ':' . $realm . ':' . $pass);
}

//////////   ここからphpメイン   ///////////////////////

$userID="TokyoTaro";
$password="himitsudesu";
$realm = 'Restricted area';
$A1=makeA1($userID,$password,$realm);
$mydatabase['userID']=$userID;
$mydatabase['A1']=$A1;
//本当は$A1はあらかじめ求められて,ファイルなどに保存されている。

$receivedUserID=authentification($realm);
//関数authentification()内でexitせずに出てこられたら認証成功
?>

<html>
<body>
<?php
print "<h1>認証されました</h1>\n";
print "<h1>".$receivedUserID."さん,こんにちは</h1>\n";
?>
</body>
</html>


参考
あるディレクトリ内のすべてのファイルに対して(サブディレクトリも含めて)ダイジェスト認証をしたい場合は,Apacheの持っているダイジェスト認証を用いることができる。
おまけApacheのダイジェスト認証



2.ファイルのダウンロード


WWWサーバから,クライアントPCへファイルのダウンロードを行う。
.phpのあるディレクトリがカレントディレクトリになっているため,
ダウンロードするファイルのパス名は,そこからの相対パスか,絶対パスで指定することになる。

ファイルのダウンロード関数を使えばよい。ただし,ダウンロードのときはprint文などで何かを表示してはいけない。表示すると,それはファイルの一部とみなされてしまう。
ここでは,自分自身をダウンロードしているが,自分の存在するディレクトリがカレントディレクトリになるので,ファイル名だけ指示している。

大きなサイズのファイルをダウンロードするときは,バッファがあふれないように,下に示す方法でダウンロード関数を作成することになる。

list 2.1 ファイルダウンロードPHPサンプル
<?php
function filedownload($fname)
{
    $file_length = filesize($fname);
    header("Content-Disposition: attachment; filename=\"" . $fname . "\"");
    header("Content-Length: " . $file_length);
    header("Content-Type: application/octet-stream");
    header('Content-Transfer-Encoding: binary');
    readfile ($fname);
    exit;
}

//////////   ここからphpメイン   ///////////////////////

$filename=basename($_SERVER['PHP_SELF']); //自分自身のファイル名(phpファイル)

if(isset($_POST['ikeike_download'])) {  //ダウンロードボタンが押された
    //ここで$filenameをダウンロードする
    filedownload($filename);
}

?>

<html>
<body>
<h1>自分自身(phpファイル)をダウンロード</h1>
<?php print "このphpのファイル名は「" . $filename . "」です。<br><br>\n"; ?>
<form method="post">
<input type="submit" name="ikeike_download" value="ダウンロード">
</form>
</body>
</html>

//大きなサイズのファイルの場合には送信バッファがあふれるため,
//このように分割してダウンロードする。
function filedownload($filename)

{
    $file_length = filesize($filename);
    header("Content-Disposition: attachment; filename=\"" . $filename . "\"");
    header("Content-Length: " . $file_length);
    header("Content-Type: application/octet-stream");
    header('Content-Transfer-Encoding: binary');

    $handle = fopen($filename, 'rb'); 
    while (!feof($handle) and (connection_status() == 0) )
    {
        echo fread($handle, 4096); 
        ob_flush(); 
        flush(); 
    }
    fclose($handle); 
}







3.ファイルのアップロード


クライアントPCからWWWサーバへファイルをアップロードする。
.phpのあるディレクトリがカレントディレクトリになっているため,保存先を指定する場合は,
相対パスまたは絶対パスを用いる。

3.1 単一ファイルのアップロード


ブラウザで送信したいファイルを1つだけ指定して送信することで,ファイルをアップロードできる。
保存先は,このphpファイルと同じカレントディレクトリとする。作業終了後,.phpファイルと同じディレクトリに
アップロードされたファイルが保存される。
送り側と受け取り側で,共通の名前(箱の名前)は「trnsFile」になっている。

このサンプルは簡略化のために,どのようなファイルでもアップロードできてしまう。
.phpファイルのような実行可能なファイルをアップロードされたら,悪意を持ったユーザがシステムを破壊することも出来てしまうので
気をつける必要がある。

list 3.1.1 ファイルアップロードPHPサンプル
<?php
if( isset( $_FILES["trnsFile"]["error"] ) ) {
    if( $_FILES["trnsFile"]["error"] === UPLOAD_ERR_OK ) {
        $tmp_name = $_FILES["trnsFile"]["tmp_name"]; //アップロードされた一時ファイル名
        $name     = $_FILES["trnsFile"]["name"]; //アップロードされたファイル名
        move_uploaded_file( $tmp_name, "./$name" );
    }
}
?>

<!DOCTYPE html>
<html>
<body>
ファイルアップロード<br>
<form method="post" enctype="multipart/form-data">
<div>
<input id="FileUpload" type="file" name="trnsFile"> 
</div>
<div><input type="submit" value="ファイル転送" /></div>
</form>
</body>
</html>


ファイル指示の画面表示例(ファイル転送ボタンを押してもなにもしません)
 IEでは「参照ボタン」でファイルセレクタが開くので,アップロードしたいファイルを選択する。
 chromeなどでは,「ファイルを選択」ボタン位置にアップロードしたいファイルアイコンをドロップすることができる。

ファイルアップロード




次の例は,テキストファイル(xxxx.txt)のみアップロードできるものとし
アップロードされたファイルの内容を表示した後,すぐに消去するものとする。

list 3.1.2 テキストファイルアップロードPHPサンプル
<?php
function fileUpload()
{
    $result['message']="";   //ファイル受信時のメッセージ
    $result['filename']="";  //アップロードされたファイル名
    $result['status']=false; //受信の成功・失敗
   
    if (isset($_FILES["ikeike_upfile"]["error"])){
        if ($_FILES["ikeike_upfile"]["error"] === UPLOAD_ERR_OK ) {
            $result['filename']=$_FILES["ikeike_upfile"]["name"];
            $uploadtmpfile=$_FILES["ikeike_upfile"]["tmp_name"];
            $result['message'].='$_FILES["ikeike_upfile"]["name"] = '. $result['filename'] ."<br>\n";
            $result['message'].='$_FILES["ikeike_upfile"]["name"] = '. $uploadtmpfile ."<br>\n";
            if (!(strrpos($result['filename'],".txt")===false)) {
                move_uploaded_file($uploadtmpfile, $result['filename']);
                $result['message'].="ファイル「" . $result['filename'] . "」がアップロードされました。<BR>";
                $result['status']=true;
            } else {
                $result['message'].=".txtファイルではないのでアップロードされません。<BR>";
            }
        } else {
            $result['message'].="ファイルアップロードでエラーがありました。<BR>";
        }
    }
    return $result;
}

////ここからstart////

$result['status']=false;
$pagetoshow=0;

if (isset($_POST['ikeike_upload'])) {
    $result=fileUpload();
    $pagetoshow=1;
}

if ($result['status']) {
    $contents=file($result['filename']);
    unlink($result['filename']);
}
?>

<html>
<body>
<?php if ($pagetoshow==0) { ?>

<h1>テキストファイルをアップロードします</h1>

<form method="post" enctype="multipart/form-data">
.txtファイル:<br>
<input type="file" name="ikeike_upfile" size="80"><br>
<input type="submit" name="ikeike_upload" value="アップロード">
</form>

<?php } else if ($result['status']) {?>

<h1>テキストファイルをアップロードしました</h1>
<?php print $result['message']; ?>
<br>
内容は次の通りでした。<br><br>
<font color="blue">
<?php
foreach($contents as $str) {
    //$str1=mb_convert_encoding($str, "utf-8", "SJIS");
    //文字コードをs-jisからutf-8に変更しながら表示する
    //print $str1."<br>\n";
    print $str."<br>\n";
}
?>
</font>
<br>
上記ファイルをWebサーバから消去しました。<br>
<?php } else {?>

<h1>テキストファイルアップロードがうまくできませんでした</h1>
<?php print $result['message']; ?>

<?php }?>

</body>
</html>


失敗する場合について

関数move_uploaded_file()でエラーを起こす,あるいはアップロードは成功するが読み出せない事がある。それは,ファイルアップロード時にテンポラリディレクトリを使うが,そのディレクトリのパーミッション上の問題である。
テンポラリディレクトリを独自に作ってphp.iniに設定すれば良い。

  テンポラリディレクトリ c:\apache2.2\ht_tmp を作り
  php.ini に upload_tmp_dir ="/apache2.2/ht_tmp" を設定する。


あるいは,単に受け取りディレクトリ内でファイルコピーだけで解決することもある
move_uploaded_file($uploadtmpfile, $uploadfile);

を次の3行に置き換える

move_uploaded_file($uploadtmpfile, "tmptmptmp00");
copy("tmptmptmp00", $uploadfile);
unlink("tmptmptmp00");


3.2 複数ファイルのアップロード


複数のファイルを指示して,同時に複数のファイルをアップロードする。
HTML5の規格では複数ファイルのアップロードができるようになった。しかしそのままでは送信時に転送するファイル名がわからないので,javascriptで表示できるようにした。骨組みだけなので,エラー対策はしていない。

作業終了後,.phpファイルと同じディレクトリにアップロードされたファイルが保存される。

list 3.2.1 複数ファイルアップロードPHPサンプル

<?php

if( isset( $_FILES["trnsFiles"]["error"] ) ) {
    foreach( $_FILES["trnsFiles"]["error"] as $key => $UStatus )
    {
        if( $
UStatus === UPLOAD_ERR_OK )
        {
            $tmp_name = $_FILES["trnsFiles"]["tmp_name"][ $key ];
            $name     = $_FILES["trnsFiles"]["name"][ $key ];
            move_uploaded_file( $tmp_name, "./$name" );
        }
    }
}

?>

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=S-JIS" />
<title>example</title>

<script>
if(window.File && window.FileReader) {
    //alert("File APIをサポートしています。");
}else{
    alert("File APIをサポートしていません。");
}

//onchangeイベントを登録
window.onload = function(){
    $("#multiFileUpload").addEventListener(
    'change', multiFileUpload_changeHandler);
}

function multiFileUpload_changeHandler(e){
    var files = e.target.files;
    var fileInfo = "";
    for(var i = 0; i < files.length; i++){
        fileInfo +=escape(files[i].name) + '<br>';
    }
    $('#info').innerHTML = fileInfo;
}
 
function $(id) {
    return document.querySelector(id);
}
</script>

</head>

<body>
複数ファイルアップロード
<form method="post" enctype="multipart/form-data">
<div>
<input id="multiFileUpload" type="file" name="trnsFiles[]" multiple> 
</div>
<table cellpadding="10"><tr><td id="info"></td></tr></table>
<div><input type="submit" value="ファイル転送" /></div>
</form>
</body>
</html>

ファイル指示の画面表示例(ファイル転送ボタンを押してもなにもしません)
 IEでは「参照ボタン」でファイルセレクタが開くので,アップロードしたいファイルを複数選択する。
 chromeなどでは,「ファイルを選択」ボタン位置にアップロードしたい複数のファイルアイコンをドロップすることができる。
複数ファイルアップロード





3.3 jQuery(javascriptのライブラリ)を用いた複数ファイルのアップロード


jQuery(javascript)を用いると,ドラッグ&ドロップでファイルを選択してアップロードすることのできる仕組みを簡単に作れる。
サンプルによる説明は別ページヘ LINK


4.排他的ファイル書き出し

サーバ側で,ブラウザのリクエストに応じて,特定のファイルに書き出す作業があり,複数のブラウザからリクエストが来ると同一ファイルへの書き出し作業が競合し,書きだしたファイルの破損が起こることがある。
そのようなことを防ぐため,1つのファイル書き込み作業が実行されている間,同一ファイルへの別の書き出し作業を待たせる仕組みを使う。

関数flock()は指定したファイルへの書き込み権限を得てから,書き出し作業を行うことを保証し,他の同一ファイルへの書き込み作業を待たせる役割を果たす。
関数flock()を使ったファイル追記の方法は次のようになる。3を忘れると,バッファ内に残る内容がfcloseの時点で書き出されることになるが,この時すでに他の作業がファイルを使用していると書き出しに失敗する。
  1. $fp=fopen($fname,"a"); ファイルのオープン(追記モード)
  2. flock($fp,LOCK_EX); ロック(ロックできたらすぐに戻ってくる。出来なかったら待たされる)
    ・・書き込み作業・・
  3. fflush($fp); バッファ内を出力し空にする
  4. flock($fp,LOCK_UN); ロック解除 (もし他の作業が待っていたら,ここで作業を許す)
  5. fclose(fp); ファイルのクローズ
次の例では,排他制御を使ってのファイル書き出しと直後読み込みを行なっている。

list 4.1 ファイルの排他的書き出し PHPサンプル
<?php
//////////   ここからphpメイン   ///////////////////////

$pagetoshow=0;
$msg="";
$msg2="";
$fname="flocktest.txt";
$nLines=0;

if (($fp=fopen($fname, "a")) !== false ) { //ファイルの追記モードでのオープン
    if (flock($fp, LOCK_EX)==true) { //ファイルのロック 他のスレッドが使用中の場合は待たされる
        fprintf($fp,"Hello, this is a test line.\n");
        fflush($fp); //バッファに残っているものを書き出す
        flock($fp, LOCK_UN); //ロック解除
    } else { //ここには来ない
        $msg.="** error ファイル".$fname."のロックに失敗しました。<br>";
    }
    fclose($fp); //ファイルのクローズ
} else {
    $msg.="** error ファイル".$fname."のオープンに失敗しました。<br>";
}

//再読出
if (($fp=fopen($fname, "r")) !== false ) { //ファイルのオープン
    $i=0;
    while (($lines[$i]=fgets($fp)) !== false) $i++;
    $nLines=$i;
    fclose($fp); //ファイルのクローズ
    if ($nLines==0) {
        $msg2.="** error ファイル".$fname."の再読出オープンは成功しましたが,何も読み出せませんでした。<br>";
    }
} else {
    $msg2.="** error ファイル".$fname."の再読出オープンに失敗しました。<br>";
}

?>

<html>
<body>
<h1>flockによる排他的ファイル書き込みのテスト</h1>

<?php

print $msg . "<br>\n";
if ($nLines==0) {
    print $msg2 . "<br>\n";
} else {
    print $fname."に書き込んで読みだした内容は以下のとおりです。<br>";
    foreach($lines as $str) {
        print $str."<br>\n";
    }
}

?>

</body>
</html>

実行結果のブラウザ表示
flockによる排他的ファイル書き込みのテスト

flocktest.txtに書き込んで読みだした内容は以下のとおりです。
Hello, this is a test line.






次の例では,ファイル書き出し作業中に20秒間のsleepをいれて,排他動作を確認している。
実用時にはsleep(20);を削除して使う。


list 4.2 ファイルの排他的書き出し PHPサンプル
<?php
//////////   ここからphpメイン   ///////////////////////

$pagetoshow=0;
$msg="";
$msg2="";
$fname="flocktest.txt";
$nLines=0;

//ここで受け取った文字列を20秒かけて保存する。
if(isset($_POST['ikeike_write'])) {
    $pagetoshow=1;
    $requestmsg="accepted message ".date('h:i:s')."\r\n";
    if (($fp=fopen($fname, "a")) !== false ) { //ファイルのオープン
        if (flock($fp, LOCK_EX)==true) { //ファイルのロック 他のスレッドが使用中の場合は待たされる
            $msg.=$fname."のオープンとロックに成功しました。<br>";
            fprintf($fp,$requestmsg);
            fprintf($fp,"-- opened and locked %s\r\n",date('h:i:s')); //時刻の書き出し
            sleep(20); //ファイルをオープンしたまま20秒何もしない(動作確認のため)
            fprintf($fp,"-- %s\r\n",$_POST['mytext']);
            fprintf($fp,"-- will be unlocked and closed %s\r\n",date('h:i:s')); //時刻の書き出し
            fflush($fp); //バッファに残っているものを書き出す
            flock($fp, LOCK_UN); //ロック解除
            $msg.=$fname."をアンロックして,クローズしました。<br>";
        } else { //ここには来ない
            $msg.="** error ファイル".$fname."のロックに失敗しました。<br>";
        }
        fclose($fp); //ファイルのクローズ
    } else {
        $msg.="** error ファイル".$fname."のオープンに失敗しました。<br>";
    }

    sleep(1);
    //再読出
    if (($fp=fopen($fname, "r")) !== false ) { //ファイルのオープン
        $i=0;
        while (($lines[$i]=fgets($fp)) !== false) $i++;
        $nLines=$i;
        fclose($fp); //ファイルのクローズ
        if ($nLines==0) {
            $msg2.="** error ファイル".$fname."の再読出オープンは成功しましたが,何も読み出せませんでした。<br>";
        }
    } else {
        $msg2.="** error ファイル".$fname."の再読出オープンに失敗しました。<br>";
    }
}

?>

<html>
<body>
flockによる排他的ファイル書き込みのテスト<br>

<?php if ($pagetoshow==0) {?>
    <p>
    なにか文字列を書いて,送信ボタンを押すと,20秒後に書き込みが終了します。
    それまでそのまま待ってください。
    </p>
    <form method="post">
    <input type="text" name="mytext">
    <input type="submit" name="ikeike_write" value="送信">
    </form>
<?php } else {
    print $msg . "<br>\n";
    if ($nLines==0) {
        print $msg2 . "<br>\n";
    } else {
        print "現在の".$fname."の内容は以下のとおりです<br>";
        foreach($lines as $str) {
            print $str."<br>\n";
        }
    }
} ?>

</body>
</html>



テスト実行時には次のように,3つのブラウザを立ち上げて,数秒ずつ遅らせて送信ボタンを押している。

IE,Chrome,SeaMonkeyの3つのブラウザを立ち上げ,それぞれのtextboxにメッセージを書いた。
 IE        : Hello fron IE
 Chrome    : Hello from Chrome
 SeaMonkey : Hello from SeaMonkey
IE,Chrome,SeaMonkeyの順で数秒間隔で送信ボタンを押した。
受付は数秒間隔のはずである。

IEのページではIEからのリクエストに応答して,20秒間かけてファイル出力を行っている間待たされた。そして,次の表示が現れた。表示するときには次の作業(Chrome)が始まってしまったので,読み出しができない。
IEの表示
    flockによる排他的ファイル書き込みのテスト
    flocktest.txtのオープンとロックに成功しました。
    ** error ファイルflocktest.txtの再読出オープンは成功しましたが,何も読み出せませんでした。


IEからの作業により待たされていた,Chromeからのリクエストに対する応答が始まり,さらに20秒後に応答が帰ってきた。次の作業(
SeaMonkey)が始まってしまったので,読み出しができない。
Chromeの表示
    flockによる排他的ファイル書き込みのテスト
    flocktest.txtのオープンとロックに成功しました。
    ** error ファイルflocktest.txtの再読出オープンは成功しましたが,何も読み出せませんでした。


さらに20秒後,SeaMonkeyに応答が帰ってきた。
SeaMonkeyの表示
    flockによる排他的ファイル書き込みのテスト
    flocktest.txtのオープンとロックに成功しました。
    現在のflocktest.txtの内容は以下のとおりです
    accepted message 04:30:42
    -- opened and locked 04:30:42
    -- Hello from IE
    -- will be unlocked and closed 04:31:02
    accepted message 04:30:46
    -- opened and locked 04:31:02
    -- Hello from Chrome
    -- will be unlocked and closed 04:31:22
    accepted message 04:30:51
    -- opened and locked 04:31:22
    -- Hello from SeaMonkey
    -- will be unlocked and closed 04:31:42

ここでようやくファイルの読み出しができている。

すべての実行が約1分かけて終わった。その時得られたflocktest.txtは次のようになった。
リクエストの受付はほぼ同時刻で,書き込み作業に20秒ずつ費やしているのがわかる。

flocktest.txt
accepted message 04:30:42
-- opened and locked 04:30:42
-- Hello from IE
-- will be unlocked and closed 04:31:02
accepted message 04:30:46
-- opened and locked 04:31:02
-- Hello from Chrome
-- will be unlocked and closed 04:31:22
accepted message 04:30:51
-- opened and locked 04:31:22
-- Hello from SeaMonkey
-- will be unlocked and closed 04:31:42


書き出し読み込みの両方でテストする。


5.クッキーの利用


クッキー(cookie)は,WWWサーバとクライアントブラウザが,一定期間連続性を持ってつながることのできる仕組みの1つである。

クッキーを使うとブラウザは次のような動作になる。
サーバから変数名「nickname」にて値「東京太郎」を3分間持ち続けなさいという コマンドをもらうと,ブラウザは3分間以内に,同じURLに接続する際に変数名「nickname」にて値「東京太郎」をサーバアクセス時に自動的に送る ようになる。(変数名と値は任意)
この
サーバから変数名「nickname」にて値「東京太郎」を3分間持ち続けなさいというコマンドは,
    setcookie("nickname","東京太郎",time()+180);
のように書く。(time()+180というのは現在時刻を単位秒で得る関数の値+180秒の意味)

また,サーバ側は,ブラウザが次の接続時に送ってくる変数名「nickname」と値「東京太郎」を受け取り,
PHPでは$_COOKIE['nickname']という変数で使えるようになる。

注意 クッキーを与えた次の回のアクセス時から
$_COOKIE['xxxx']は使えるようになる。
   WWWサーバは,持続して何かを覚えている仕組みは持っていない。

次のテストプログラムは,nicknameとアクセス回数countの2つをクッキーとして扱い,ニックネームと接続回数を表示するものである。


list 5.1 クッキーの利用 PHPサンプル
<?php
//////////   ここからphpメイン   ///////////////////////

$pagetoshow=0;
$expire=time()+180;  //有効期限 現在時刻+180秒

if(isset($_POST['ikeike_cookie'])) { //送信ボタンが押された
    $nickname=htmlspecialchars($_POST['nickname'], ENT_QUOTES);
    $pagetoshow=1;
    setcookie("nickname",$nickname,$expire); //ブラウザPCに保存要求
    setcookie("count","1",$expire);          //ブラウザPCに保存
要求
} else if (isset($_COOKIE['nickname'])) { //cookieにnicknameがあった
    $pagetoshow=2;
    $nickname=$_COOKIE['nickname'];   //ブラウザPCから受け取る
    $count=$_COOKIE['count']+1;
    setcookie("nickname",$nickname,$expire); //ブラウザPCに保存
要求
    setcookie("count",$count,$expire);       //ブラウザPCに
保存要求
}

?>

<html>
<body>
<h1>cookieテスト</h1>

<?php if ($pagetoshow==0) {?>
    <p>
    あなたのニックネームを書いて,送信ボタンを押してください。<br>
    </p>
    <form method="post">
    <input type="text" name="nickname">
    <input type="submit" name="ikeike_cookie" value="送信">
    </form>
<?php } else if ($pagetoshow==1) { ?>
    <?php
    print "はじめまして,<b>" . $nickname . "</b>さん。<br>\n";
    print "<b>1</b>回目のアクセスです。<br><br>\n";
    ?>
    <p>
    ニックネームとアクセス回数を,有効期間3分間のクッキーとして,ブラウザ側に設定してあります。<br>
    ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。<br>
    有効期間内であれば,ブラウザから,ニックネームとアクセス回数を受け取って表示します。<br>
    </p>
<?php } else { ?>
    <?php
    print "こんにちは,<b>" . $nickname . "</b>さん。<br>\n";
    print "<b>" . $count . "</b>回目のアクセスです。<br>\n";
    ?>
    <p>
    ニックネームと訪問回数を,有効期間3分間のクッキーとして,ブラウザ側に設定してあります。<br>
    ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。<br>
    有効期間内であれば,ブラウザから,ニックネームとアクセス回数を受け取って表示します。<br>
    </p>
<?php } ?>

</body>
</html>


実行の様子
cookieテスト

あなたのニックネームを書いて,送信ボタンを押してください。


-------------------------------------------------------------------------------------------
cookieテスト

はじめまして,東京太郎さん。

1回目のアクセスです。

ニックネームとアクセス回数を,有効期間3分間のクッキーとして,ブラウザ側に設定してあります。
ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
 有効期間内であれば,ブラウザから,ニックネームとアクセス回数を受け取って表示します。

-------------------------------------------------------------------------------------------
cookieテスト

こんにちは,東京太郎さん。

2回目のアクセスです。

ニックネームと訪問回数を,有効期間3分間のクッキーとして,ブラウザ側に設定してあります。
ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
 有効期間内であれば,ブラウザから,ニックネームとアクセス回数を受け取って表示します。

-------------------------------------------------------------------------------------------
cookieテスト

こんにちは,東京太郎さん。

3回目のアクセスです。

ニックネームと訪問回数を,有効期間3分間のクッキーとして,ブラウザ側に設定してあります。
ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
 有効期間内であれば,ブラウザから,ニックネームとアクセス回数を受け取って表示します。

-------------------------------------------------------------------------------------------
cookieテスト

こんにちは,東京太郎さん。

4回目のアクセスです。

ニックネームと訪問回数を,有効期間3分間のクッキーとして,ブラウザ側に設定してあります。
ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
 有効期間内であれば,ブラウザから,ニックネームとアクセス回数を受け取って表示します。


実行の様子のイメージ



6.セッションの利用


セッションもWWWサーバとクライアントブラウザが,一定期間連続性を持ってつながることのできる仕組みの1 つである。クッキーが,変数をクライアントブラウザ側に持たせるのに対し,セッションでは変数をWWWサーバが持つようになる。ただし,WWWサーバとク ライアントブラウザの接続を保つために,ランダム文字列の合言葉クッキーを1つだけ使う。

・セッションを開くときには,
(1)WWWサーバ側にセッション環境を作る。サーバに合言葉(
セッションID)が設定され保持される。この合言葉はWWWサーバが長いランダム文字列として生成する。
 
(2)WWWサーバはクライアントブラウザに合言葉セッションIDクッキーを与える。
WWWサーバのPHPプログラムが明示的にこの2つの細かな作業を行う必要はない。
この作業は1命令
で行われる。

・セッションが継続中のとき,
クライアントブラウザがセッションを保持しているWWWサーバにアクセスすると
次の2つのことが自動的に行われる。
(1)
クライアントブラウザはアクセスするときに合言葉クッキーを送信する
(2)合言葉クッキーを受け取ったWWWサーバはセッションの合言葉と照合して,一致したらセッションを再開する。
WWWサーバのPHPプログラムが明示的に合言葉の一致を照合する必要はない。
セッション再開作業も1命令で行われる。

・セッションを閉じるときは,
WWWサーバのPHPプログラムが明示的に,
(1)セッション環境の廃棄する。(サーバの合言葉は破棄される)
(2)
クライアントブラウザに合言葉クッキーを削除させる。
を行わなければならない。

session_start() は,既存のセッションがない場合は,
「新規セッション」を作り,そうでない場合は「既存セッションを利用可能」にする。
・既存のセッションの有無は,
受け取った合言葉クッキーの合言葉が,WWWサーバで保持している合言葉中に一致するものが存在するかどうかで判断される。
・「新規セッション」を作るとは,合言葉を生成し,WWWサーバに合言葉で参照できるように保存場所を確保し,クライアントブラウザに合言葉クッキーを送るという作業である。この時セッション変数$_SESSION(連想配列)がその既存セッション内で利用できるようになる。
・「既存セッションを利用可能」にするとは受け取った合言葉クッキーとサーバ側で保存している合言葉を照合し,一致していたらセッション変数$_SESSIONを復活させ,使えるようにするという意味である。


注意


list 6.1 セッションの利用 PHPサンプル
<?php
//////////   ここからphpメイン   ///////////////////////

$pagetoshow=0; //0:loginを表示 1;logoutボタンを表示 2:ボタンを表示しない
$message="";

if (isset($_POST['ikeike_open'])) { //ログインボタンが押された
    $lifetime=60; //テストのため60秒
    session_set_cookie_params($lifetime);
    session_start();
    //セッションを作成する
    $pagetoshow=1;
    $count=1;
    $_SESSION["count"] = $count;            //サーバに値を保存
    $nickname = htmlspecialchars($_POST['nickname'], ENT_QUOTES);
    $_SESSION["nickname"] = $nickname;      //サーバに値を保存
    $date = date('c');
    $_SESSION["date"] = $date;              //サーバに値を保存
    $message .= 'セッションをオープンしました。'."タイムアウトは $lifetime [秒]です。";
} else if(isset($_POST['ikeike_close'])) {  //ログアウトボタンが押された
    $_SESSION = array();//現在のセッション変数をクリアする
    session_start();
    //セッション用クッキーをタイムアウトにする
    $pagetoshow=2;
    if (isset($_COOKIE[session_name()])) {
        setcookie(session_name(), '', time() - 1800, '/');
    }                                       // cookieの削除
    session_destroy();                      //セッションの終了
    $message .= 'セッションをクローズしました。';
} else if (isset($_COOKIE[session_name()])) { //$_COOKIE[session_name()]があった
    session_start();
    //既存のセッションを使用可能にする
    $pagetoshow=1;
    $message .= 'セッションはオープンしています。<br>';
    $count=$_SESSION["count"]+1;
    $_SESSION["count"] = $count;            //サーバに値を保存
    $nickname = $_SESSION["nickname"];
    $date = $_SESSION["date"];
    $message .= 'セッション名は。' . session_name() . 'です。<br>';
    $message .= 'クッキーは '.$_COOKIE[session_name()].' です。<br>';
    $message .= 'session_id()は '.session_id().' です。<br>';
} else {
    //初回は必ずここを通過する
    $pagetoshow=0;
    $message="はじめまして!";
}


?>

<html>
<body>
<h1>sessionテスト</h1>

<?php if ($pagetoshow==0) { ?>
    <?php print $message; ?>
    <p>
    あなたのニックネームを書いて,ログインボタンを押してください。<br>
    </p>
    <form method="post">
    <input type="text" name="nickname">
    <input type="submit" name="ikeike_open" value="ログイン">
    </form>
<?php } else if ($pagetoshow==1) { ?>
    <?php
    print "こんにちは,<b>" . $nickname . "</b>さん。<br>\n";
    print "<b>" . $count . "</b>回目のアクセスです。<br>\n";
    print "ログインは<b>" . $date . "</b>でした。<br><br>\n";
    print $message;
    ?>
    <p>
    ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。<br>
    サーバのセッション変数から,ニックネームとアクセス回数を受け取って表示します。<br>
    </p>
    <p>
    ログアウトするときはログアウトボタンを押してください。<br>
    </p>
    <form method="post">
    <input type="submit" name="ikeike_close" value="ログアウト">
    </form>
<?php } else { ?>
    <p>
    ログアウトしました。<br>
    ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。<br>
    再度ログインできる状態になります。<br>
    </p>
<?php } ?>

</body>
</html>


実行の様子
sessionテスト

はじめまして!

あなたのニックネームを書いて,ログインボタンを押してください。


-------------------------------------------------------------------------------
★ここでログインボタン
-------------------------------------------------------------------------------
sessionテスト

こんにちは,東京太郎さん。
1回目のアクセスです。
ログインは2014-04-07T15:41:27+09:00でした。

セッションをオープンしました。
タイムアウトは 60 [秒]です。

ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
サーバのセッション変数から,ニックネームとアクセス回数を受け取って表示します。

ログアウトするときはログアウトボタンを押してください。


-------------------------------------------------------------------------------
sessionテスト

こんにちは,東京太郎さん。
2回目のアクセスです。
ログインは2014-04-07T15:41:27+09:00でした。

セッションはオープンしています。
セッション名は。PHPSESSIDです。
クッキーは tb3s9fg5o89sf4rmm2dpko32q0 です。
session_id()は tb3s9fg5o89sf4rmm2dpko32q0 です。

ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
サーバのセッション変数から,ニックネームとアクセス回数を受け取って表示します。

ログアウトするときはログアウトボタンを押してください。


-------------------------------------------------------------------------------
sessionテスト

こんにちは,東京太郎さん。
3回目のアクセスです。
ログインは2014-04-07T15:41:27+09:00でした。

セッションはオープンしています。
セッション名は。PHPSESSIDです。
クッキーは tb3s9fg5o89sf4rmm2dpko32q0 です。
session_id()は tb3s9fg5o89sf4rmm2dpko32q0 です。

ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
サーバのセッション変数から,ニックネームとアクセス回数を受け取って表示します。

ログアウトするときはログアウトボタンを押してください。


-------------------------------------------------------------------------------
★ここでログアウトボタン
-------------------------------------------------------------------------------
sessionテスト

ログアウトしました。
ブラウザを開きなおすか,別のタブを開いて,同じURLにアクセスしてください。
 再度ログインできる状態になります。

-------------------------------------------------------------------------------
sessionテスト

はじめまして!

あなたのニックネームを書いて,ログインボタンを押してください。



実行の様子のイメージ


セッション情報はファイルに保存される。
最初のsession_start()により,このファイルが作成され,2度目以降のsession_start()ではファイルから$_SESSION が復元される。$_SESSIONに値が保存されるたびに,値が書き込まれる。session_destroy()によってこのファイルは消去される。
このファイルはクライアントに送付した合言葉クッキーにちなんだ名前が付けられている。
このファイルの保存場所は,システムのデフォルトテンポラリディレクトリだが,php.iniに設定することもできる。
c:\apache2.2\ht_tmp
を設定したい場合は
session.save_path = "/apache2.2/ht_tmp"
をphp.iniに記述する。

ログアウトしないまま,セッション情報ファイルが残ってしまった場合は,このディレクトリ内のセッション情報ファイルを手動で消すことができる。

参考 sha256によるセッション
 WWWのdigest認証はMD5というhash関数が使われているが,これがそれほど安全でないとされている。当面安全だとされているsha256というhash関数も用いて,ユーザ認証を行いセッションを確保する例を次に載せる。
ブラウザ側でパスワードを送信する時,hash関数を使わないと,通信経路に生のパスワードが送られてしまう。そこで,パスワード送信前にjavascriptでhash関数を使って生のパスワードを見せないようにする必要がある。
「sha256.js」(http://sourceforge.net/projects/jssha/files/)
にsha256hash関数ライブラリが公開されている。


サンプルプログラム