***HPクリエイターのためのCGI講座 第45回***

4.2 クッキー対応掲示板

ここでは、クッキーについて学びます。
クッキーとは、ユーザーに固有な情報を、ユーザー側のコンピュータに記憶させる仕組みです。
ここの受講者は、HP作成の経験者ですから、どのように使われているかは、大体ご存知ですよね。

<クッキーへの書き出し>
ブラウザへ次のように出力します。
    print "Set-Cookie: 〜 ";

のところには、次のフォーマットで記述します。
名前=値; expires=クッキーの有効期限(国際標準時刻で設定);

名前 自由に記述します。
自由に記述します。
expires これはキーワードです。このまま記述。
クッキーの有効期限 ユーザー側のコンピュータに記録されるCookieの有効期限を指定します。次のようなフォーマットで指定します。
Sunday, 28-May-2000 10:10:10 GMT

クッキーの書き出しは、最初に行ってください。
(例)
print "Set-Cookie: C_BBS=aaabbbccc; expires=Sunday, 28-May-2000 10:05:02 GMT ";
print "Content-type: text/html "; 
print "<html> ";
             :
             :

上記例の場合
print "Set-Cookie: C_BBS=aaabbbccc; expires=Sunday, 28-May-2000 10:05:02 GMT ";
クッキーは2000年5月28日の10時5分2秒まで、保存される
記憶されるクッキーは  C_BBS=aaabbbccc;

<クッキーからの読み出し>
環境変数HTTP_COOKIEにクッキーの内容が設定されています。

例のようにクッキーを書き込まれていた場合、$ENV{'HTTP_COOKIE'}には、次のテキストが格納されています。
C_BBS=aaabbbccc;


例示プログラムとして、投稿者の名前と、e-mailアドレスをクッキーに記憶する掲示板を作成しました。
この掲示板では、まだまだ力不足ですが、掲示板としての最低限の仕事はするでしょう。

<ログファイルのフォーマット>
この掲示板のログファイルに格納されているメッセージは、<>を区切り記号にして、名前、e-mail、タイトル、メッセージの順に書かれています。

(1メッセージの例)
alk<>alk@manabitai.com<>タイトル<>投稿内容

<ヒアドキュメント>
今回のCGIプログラムでは、HTML文章の作成にprint文の特殊な形、ヒアドキュメントを使っています。
ヒアドキュメントの説明はここを参照してください

<プログラムの説明>
require '../../../cgi/jcode.pl';
日本語コード変換プログラムの場所を指定

$lock_file = "../../../cgi/c_bbs.lock";
多重アクセス禁止用ロックファイル名

***********各種設定************
$top_title = 'BBS'; # BBSのタイトル
$msg_log = 'msg_log.txt'; # メッセージログファイル名
$log_num = 10; # メッセージの格納数(1ページの表示数も兼ねる)
$msg_max = 500; # 1回の最大メッセージ数(半角文字数で指定)

#処理のスタート
&in_para; # 入力パラメータの取得
入力パラメータ取得用サブルーチンをコール。
入力パラメータを受け取り、デコードします。

&get_cook;

クッキーの読み込み

if ($in_btn ne ""){ # ボタンが押されて、このCGIが起動された

    &log_write;  # ログファイルへメッセージの書込み
}
前回の掲示板プログラムと同様に、このCGIが起動される要因として、2種類あります。
1.オープニング時に呼び出される
    「サンプルはここ」と記述された「ここ」をクリックした場合
2.掲示板の書込みボタンを押した場合

書込みボタンが押された場合は、 $out に "書込み" の文字列が格納されています。
書込みボタンが押されていない場合は、 $out には何も格納されていません。
このCGIプログラムの起動要因を、 $out に文字列が設定されているか否かで判定しています。

********ここからは、HTMLテキストをブラウザへ出力*********
print "Content-type: text/html\n\n"; # HTML文をブラウザへ出力
print <<"HTML";
ヒアドキュメントのはじまり

<html><head>
<title>BBS</title></head>
<center><font color="00ff00" size="6">
<b>$top_title</b></font></center><hr size=2>
<FORM method="POST" action="c_bbs.cgi">
名前<INPUT size="40" type="text" name="name" value="$c_name"><BR>
eメール<INPUT size="40" type="text" name="mail" value="$c_mail"><BR>
タイトル<INPUT size="40" type="text" name="title"><BR>
<BR>
<TEXTAREA rows="10" cols="60" name="msg"></TEXTAREA><INPUT type="submit" name="out" value="書 込 み"></FORM>
FORMタグ
    methodはPOSTで、CGI(easy_bbs.cgi)を呼び出す
    テキスト入力領域の大きさは、40文字*15行
    CGIプログラムの受け取る、テキスト入力パラメータの名前はmsg
    書込みボタンの名前はout

<hr size=2>
HTML
ヒアドキュメントの終わり

open(LOG,"<$msg_log") || &error(0); # ロググファイルの読込みモードでのオープン
ログファイル(発言内容が記述されている)を読み込みモードでオープン

@log_buff = <LOG>; # ログファイルから全て、読み出し
ログファイルの内容を一度に配列@log_buffへ読み込む。
このように、記述すると、配列の各要素に一行ずつ文字列が読み込まれます。
log_buff[0]・・・・1行目の文字列
log_buff[1]・・・・2行目の文字列
log_buff[2]・・・・3行目の文字列
                :
                :

close(LOG);
ログファイルのクローズ(読み込みが終了したので)

foreach (@log_buff){ # $_へ1行ずつ取り出す
    #ファイルには<>を区切り記号として、名前、e-mail、タイトル、メッセージの順で格納されている
    ($out_name,$out_mail,$out_title,$out_msg) = split(/<>/);
    $out_name・・・名前を格納
    $out_mail・・・メールアドレスを格納
    $out_title・・・タイトルを格納
    $out_msg・・・投稿メッセージを格納

    # 取り出した1行を編集して表示
    print "題名:$out_title名前:<a href=\"mailto:$out_mail\">$out_name</a><br><br>";
    print "$out_msg<br>";
    print "<hr size=2>\n"; # 区切り用ラインの表示
}
print "</html>\n";
exit;


ここからは、入力パラメータ取得サブルーチン
sub in_para {
    if ($ENV{'CONTENT_LENGTH'} > $msg_max){
    METHODがPOSTなので、環境変数CONTENT_LENGTHに文字列が格納されています。
    その値をチェックし、500文字以上はエラーとします。
        &error(0); # 入力メッセージの最大長を500バイト(半角500文字)とする
    }
    read(STDIN, $in_buffer, $ENV{'CONTENT_LENGTH'}); # POSTなのでパラメータを標準入力から読み出す
    標準入力STDINから、パラメータを取得します

    @pairs = split(/&/,$in_buffer); # パラメータのペアを切り出す
    パラメータは & で区切って送られてきます。
    配列@pairsの各要素に & で区切った文字列を格納します。

    foreach $pair (@pairs) {
        ($name, $value) = split(/=/, $pair); # パラメータを名前と値に分解
        各パラメータは 名前=値 の形をしています。
        それぞれを$nameと$valueへ格納します。
        $value =~ tr/+/ /; # コードのデコード
        変換規則に従って+はブランクに置換
        $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
        16進文字列を、数値に置換
        &jcode'convert(*value,'sjis'); #  S-JIS変換
        得られた文字列を、S-JISに変換
        $value =~ s/\"/&quot;/g; # タグ文字の変換
        特殊文字の変換
        "(ダブルクォテーション)は&quot;に置換
        $value =~ s/</&lt;/g;
        < は&lt; に置換
        $value =~ s/>/&gt;/g;
        > は&gt; に置換
        if($name eq "name"){
            現在のパラメータの処理がname(名前)の場合
            $in_name = $value; # 名前
        }
        elsif($name eq "title"){
            現在のパラメータの処理がtitle(タイトル)の場合
            $in_title = $value; # タイトル
        }
        elsif($name eq "mail"){
            現在のパラメータの処理がmail(メールアドレス)の場合
            $in_mail = $value; # e-mailアドレス
        }
        elsif($name eq "msg"){ # 入力メッセージ
            現在のパラメータの処理がmsg(投稿記事)の場合
            $in_msg = $value;
            記事を$msgに格納します。

            現在のパラメータの処理がmsg(投稿記事)の場合は改行コードを<br>に置換します。
            (改行コードには、次の3種類がある)
            $in_msg =~ s/\r\n/<br>/g; # リターンコードを<br>に置換
            $in_msg =~ s/\r/<br>/g; #     "
            $in_msg =~ s/\n/<br>/g; #     "
        }
        elsif($name eq "out"){
            現在のパラメータの処理が書込みボタンの場合
            $outに文字列(書込み)を格納します。
            $in_btn = $value; # ボタンに表示している文字が格納される 今回の場合は「書込み」
        }
    }
}

ここからは、投稿記事のログファイルへの書込み処理
sub log_write {
    &lock; #  ロック処理
    多重アクセス排他処理

    open(LOG,"<$msg_log") || &error(1); # ログファイルを読込みモードでオープン
    ログファイルを読み込みモードでオープンします。
    @log_buff = <LOG>; # 全メッセージの読み出し
    ログファイルの内容を@log_buffへ読み出します
     close(LOG);
    ログファイルのクローズ
    if ($log_num <= @log_buff){
        ログファイルへの最大格納メッセージ数は$log_num(10件)です。
        現在のログファイルに格納されているメッセージ数が10件なので
        pop(@log_buff); # メッセージが$log_numを超えれば、一番古いメッセージを削除する
        一番古いメッセージを配列@log_buffから pop 命令を使って、削除します。
    }
    unshift (@log_buff,"$in_name<>$in_mail<>$in_title<>$in_msg\n");
    今、取り込んだメッセージを unshift 命令を使って配列@log_buffの先頭に格納します。

    open(LOG,">msg_log.txt") || &error(1);  # ログファイルを書込みモードでオープン
    ログファイルを書込みモードでオープン
    print LOG @log_buff; # メッセージのログファイルへ書込み
    配列@log_buffをログファイルへ書き出します。
    close(LOG);
    ログファイルをクローズします。
    if (-e $lock_file){
        unlink($lock_file); # ロックの解除
    }
    &put_cook; # クッキーへの書き出し
}

ここからはクッキーへの書込み処理
sub put_cook { 
    # クッキーの有効期限のために
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time + 10*24*60*60);
    標準時刻を読み込みます。

    年月日時分秒の設定
    日時分秒が一桁の場合は、前に 0 を付けて2文字にします。
    $year += 1900; #年は1900年からの年数なので、1900を足す
    if ($sec  < 10){$sec  = "0$sec";} # 前に0を付ける
    if ($min  < 10){$min  = "0$min";}
    if ($hour < 10){$hour = "0$hour";
    if ($mday < 10){$mday = "0$mday";

    $mon = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec') [$mon];
    月を、英字で取得
    $youbi = ('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday') [$wday];
    曜日を、英字で取得
    $cook_gmt = "$youbi, $mday\-$mon\-$year $hour:$min:$sec GMT";
    クッキー設定時間を格納
    <例>
    2000/8/9(水) 15:47:5の場合
    Wednesday,09-Aug-2000 15:47:05 GMT

    $cook_txt="name\:$in_name\,mail\:$in_mail";
    $cook_txtに名前とメールアドレスを格納します。
    <例>
    $cook_txtには次のように格納される
    name:ALK.mail:alk@manabitai.com

    print "Set-Cookie: C_BBS=$cook_txt; expires=$cook_gmt\n";
   クッキーへの書込み
    <例>
    Set-Cookie: C_BBS=name:ALK.mail:alk@manabitai.com; expires=Wednesday,09-Aug-2000 15:47:05 GMT
}

ここからはクッキーからの読み出し処理
sub get_cook {
    @cook_paras = split(/\;/,$ENV{'HTTP_COOKIE'});
    クッキーは環境変数 $ENV{'HTTP_COOKIE'} に格納されます。
    また、クッキーは;(セミコロン)で区切られたテキストで入ってきます。
    今回は、クッキーへの書込み処理を見てもらえれば分かるように1回だけです。
    $cook_paras[0]には次のような文字列が格納されてきます。
    C_BBS=name:ALK.mail:alk@manabitai.com

    foreach $cook_para (@cook_paras) {
    $cook_paras[0] を $cook_para に取り出します。

        local($name, $value) = split(/\=/, $cook_para);
        C_BBS=name:ALK.mail:alk@manabitai.comを次のように分解します。
        $name・・・C_BBS
        $value・・・name:ALK.mail:alk@manabitai.com

         $name =~ s/ //g;
        $nameのブランクを取り除きます。
        これは、処理系によってはブランクが入る可能性があるための処理
        
        if($name eq 'C_BBS'){ # C_BBSはこのBBS用クッキーである
        $nameが’C_BBS'であれば、今回の演習のBBSが書き込んだクッキーです。

            @cook_paras = split(/\,/,$value);
            パラメータは,(カンマ)で区切られているので、カンマ分解して、配列@cook_parsに設定します。
            <例>
            $cook_pars[0]・・・name:ALK
            $cook_pars[1]・・・mail:alk@manabitai.com

            foreach $cook_para (@cook_paras) {
            パラメータをひとつずつ$cook_parに取り出します

                local($name, $value) = split(/\:/, $cook_para); #クッキーのデコード
                ひとつのパラメータは:(コロン)で区切られた形をしているので、コロンで分解します。
                <例>
                $cook_parsに文字列 name:ALK が設定されていた場合
                $name・・・name
                $value・・・ALK

                if($name eq 'name'){
                $nameが name の場合
                    $c_name = $value; # 名前
                    $c_nameに$value設定されている名前を格納
                }
                elsif($name eq 'mail'){
                $nameが mail の場合
                    $c_mail = $value; # e-mailアドレス
                    $c_mailに$value設定されているマールアドレスを格納
                }
            }
            last;
            C_BBSの処理が終われば終了です。
        }
    }
}

sub lock{ # ロック処理
    $retry = 5;
    while (!symlink(".", $lock_file)){
        if (--$retry <= 0){
            &error(0);
        }
        sleep(1);
    }
}

sub error{ # エラー処理
    if((-e $lock_file) && ($_[0] != 0)){
        unlink($lock_file);
    }

    print "Content-type: text/html\n\n";
    print "<html>\n<body bgcolor=# FF0000>\n";
    print "<center><h3>ERROR</h3>\n";
    print "</html>\n";
    exit;
}


********************************************************************************
講師:ALK alk@arkland.co.jp
運営:アークランド(株) http://www.arkland.co.jp