いよいよ、プログラムのソースコードを書ける状態になりました。前回まででプログラムの流れが整理されたので、なにをすべきかということがはっきりしたと思います。あとは最初のフロー図に従ったソースを書いていくだけとなります。
[書込処理の流れ]
1.HTML書込みフォーム(HTML)
↓
2.フォームからの入力
↓
3.入力されたデータの加工、チェック
↓
4.入力されたデータの保存
(指定された以上の件数があれば古いものから削除)
↓
5.保存後の終了画面(HTML)
|
掲示板の第1回でも書きましたが、基本的なシステムの流れはこのようになるので、この流れに沿った形でプログラムを行っていくことになるのですが、それぞれのフェーズでもう少し細かいことをしていくことになるので、順番に見ていきたいと思います。
※ちなみに、、、
このページで利用するプログラムのソース(テキスト)全てはこちらからダウンロードできます。
また、ここでは
- 書き込み用Perlのファイル名を「sbbs_write.cgi」
- 読み込み用Perlのファイル名を「sbbs_read.cgi」
- フォーム用HTMLのファイル名を「sbbs.html」
として解説していきます。
この部分は第1回の概要の設計で検討したとおり、利用者から受け取るデータ項目は名前、件名、内容の3項目だとします。
データのサニタイズで、クライアント側での文字数制限は意味がない、、、旨のことを書きましたが、「クライアント側だけでしても意味がない」ということで、しないよりはしたほうがいいです。
想定される入力文字数を「maxlength属性」を利用して制限します。ブラウザによって「maxlength」の意味が違うので、解釈上一番たくさん入力できるようにmaxlengthを設定すればいいと思います。あとは、Maxlength以外にJavaScriptで文字数チェックする方法もありますが、また別の機会に実験します。。
下のようなHTMLを作成し、ファイルに保存します。
ここでは「sbbs.html」という名前で保存することにします。
01: <form action="sbbs_write.cgi" method="POST">
02: お名前 <input type="textbox" name="u_name" maxlength="20"><br>
03: 件 名 <input type="textbox" name="subject" maxlength="40"><br>
04: 内 容 <textarea name="comment"></textarea><br>
05: <input type="submit" value="送信">
06: </form>
|
上記HTMLのサンプルはこちらをクリックしてください(別窓)。
フォームの中の必要な属性が変更されていなければあとのデザインは変更しても動きます。(当然といえば当然なのですが、、、)
ここからPerl側で行う処理となります。
フォームから入力されるとPOSTかGETかで処理が変わってきますが、これは以前の実験で紹介した読み込みデータ用のサブルーチンを利用して処理することにします。
一番最初に「&read_data;」というサブルーチンを呼び出すことで、入力されたデータが$FORM{項目名}というようにハッシュ化されることは、以前の実験でしましたよね?
01: #!/usr/local/bin/perl
02:
03: # 日本語処理用のライブラリ
04: require 'jcode.pl';
05:
06: # 読み込まれたデータのハッシュ処理
07: &read_data;
|
ただ、入力された各文字を想定されるサイズであるか、また想定していない文字が入力されていないかのチェックをする必要があります。ですから、前回示したサブルーチンから若干今回用に修正する必要があります。
具体的には下のようなソースになります。
01: sub read_data(){
02: if ($ENV{'REQUEST_METHOD'} eq "POST") {
03: # 読み込まれたデータのサイズチェック
04: if ($ENV{'CONTENT_LENGTH'} > 1300) {
05: &error("入力された文字数が多すぎます。");
06: }
07: read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
08: } else {
09: &error("この呼び出し方法は許可されていません。");
10: }
11:
12: foreach (split(/&/, $buffer)) {
13: ($keyword, $value) = split(/=/);
14: $value =~ tr/+/ /;
15: $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
16:
17: # S-JISコード変換
18: &jcode'convert(*value, "sjis", "", "z");
19:
20: $value =~ s/&/&/g; # アンパサンド
21: $value =~ s/"/"/g; # 括弧のエスケープ
22: $value =~ s/\,/,/g; # カンマのエスケープ
23: $value =~ s/</</g; # タグの排除
24: $value =~ s/>/>/g; # タグの排除
25:
26: if ($keyword eq "comment"){
27: $value =~ s/\r\n/<br>/g; # 改行の処理
28: $value =~ s/\r/<br>/g;
29: $value =~ s/\n/<br>/g;
30: }else{
31: $value =~ s/\r//g;
32: $value =~ s/\n//g;
33: }
34:
35: $FORM{$keyword} = $value;
36: }
37: }
*左の数字は便宜上つけた行番号です。サブルーチン内で完結するような番号を付してます。
|
サブルーチン内の処理は以前と同じで、HTMLのFORMから「name=AAA」「value=BBB」という組み合わせで送信されたデータを「 $FORM{ AAA } = BBB 」というようにハッシュ化するためのものです。
ただ、データのサニタイズでも書きましたが、不要なデータが入力されていないか、サイズが大きすぎないかというチェックを個々でしています。具体的には、4行目でPOSTされた全体のデータサイズをチェックして大きすぎるときはエラーとなるような処理にしています。また、GETで送信された場合にも受付けないようにエラーとなるようにしています。
01: sub read_data(){
02: if ($ENV{'REQUEST_METHOD'} eq "POST") {
03: # 読み込まれたデータのサイズチェック
04: if ($ENV{'CONTENT_LENGTH'} > 1300) {
05: &error("入力された文字数が多すぎます。");
06: }
07: read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'});
08: } else {
09: &error("この呼び出し方法は許可されていません。");
10: }
|
次に、データのサニタイズの続きですが、不要なタグや改行の処理をしています。なぜこのような処理が必要かは前回検討しましたが、26行目をみてください。この部分だけ改行の処理を変更しています。普通は改行コードは邪魔なだけなのですが、入力された記事そのもの、つまりコメントの中では改行が反映される必要があるため、表示用に「<br>」というタグに変更しているのです。
20: $value =~ s/&/&/g; # アンパサンド
21: $value =~ s/"/"/g; # 括弧のエスケープ
22: $value =~ s/\,/,/g; # カンマのエスケープ
23: $value =~ s/</</g; # タグの排除
24: $value =~ s/>/>/g; # タグの排除
25:
26: if ($keyword eq "comment"){
27: $value =~ s/\r\n/<br>/g; # 改行の処理
28: $value =~ s/\r/<br>/g;
29: $value =~ s/\n/<br>/g;
30: }else{
31: $value =~ s/\r//g;
32: $value =~ s/\n//g;
33: }
|
また、それぞれの変数(名前や件名)のサイズを「maxlength」で指定していましたが、ハッシュの処理がされた後でもプログラム側でも個別に処理する場合には、下記のようなチェックをする必要があります。
07: &read_data;
08:
09: # データの長さチェック等
10: if (length($FORM{'u_name'}) > 40) { &error("名前が長すぎます。");}
11: if (length($FORM{'subject'}) > 80) { &error("件名が長すぎます。");}
12: if (length($FORM{'comment'}) > 1000) { &error("コメントが長すぎます。");}
|
データの入力サブルーチンで適正にデータを受け取ると、次はいよいよ書き込み処理です。基本的には下のソースのとおりですが、若干説明が必要な場所もあると思うので順次解説します。
14: # logfile.datというファイルから現在のカウント数を読み込む
15: open (IN, "logfile.dat") || &error("読み込みエラーが発生しました。");
16: @logdata=<IN>
17: close(IN);
18:
19: # データ数のチェック(もし100件以上なら99に変換)
20: $kazu = @logdata;
21: if ($kazu >=100) {pop(@logdata);}
22:
23: # 記事のトップデータから最新の番号の取得して、次の番号を決定
24: $no = (split(/\,/, $logdata[0]))[0];
25: $no++;
26:
27: # 時間の取得
28: ($sec,$min,$hour,$day,$mon,$year,$week) = localtime(time);
29: @weekname = ('日','月','火','水','木','金','土');
30: $date = sprintf("%04d年%02d月%02d日(%s) %02d:%02d:%02d",
$year+1900,$mon+1,$day,$weekname[$week],$hour,$min,$sec);
31:
32: # 追加するデータの作成
33: $add_data = "$no,$FORM{u_name},$FORM{subject},$FORM{comment},$date\n";
34:
35: # 配列に追加
36: unshift(@logdata,$add_data);
37:
38: # ファイルの保存
39: open (OUT, ">logfile.dat") || &error("書き込みエラーが発生しました。");
40: print OUT @logdata;
41: close(OUT);
|
14〜17行のデータ読み込み処理は問題ないと思いますが、20〜21行目の処理では何をしているのでしょうか?ここでは、書き込まれた発言の件数が100件以上の場合にはそれ以上にならないような処理をしています。
19: # データ数のチェック(もし100件以上なら99に変換)
20: $kazu = @logdata;
21: if ($kazu >=100) {pop(@logdata);}
|
「@logdata」は、掲示板の書き込みデータですが、これを20行目のように「$kazu = @logdata;」とすることで、その配列の数を取得することが出来ます。これで、「$kazu」に何件の記事がかかれているかが入力されました。
これを21行目で「100件以上なら、、、データを削除」という処理をしているのです。「pop();」というのは、配列の一番最後のデータを削除する命令です。書き込みされる度にこのチェックが行われるので、100件になった段階で一つ削除して99件のデータにしてしまいます。
次がすこし変わった表記をしています。何をしているかというと、書き込まれた記事にはその発言ごとにユニーク(一意)となるような番号を付与しています。これは通常書き込みがあるたびにその連番をつけていけばいいので、前回書き込まれた記事の番号を取り出して1足す処理をしています。
23: # 記事のトップデータから最新の番号の取得して、次の番号を決定
24: $no = (split(/\,/, $logdata[0]))[0];
25: $no++;
|
24行目を丁寧に解説するなら、、、配列「@logdata」から前回書き込まれた記事「$logdata[0]」を取り出して、「split」関数でカンマ区切りのデータをバラバラにします。それを「$no = (........)[0]」という式でバラバラにされた0番目、つまり最初のデータである記事番号を「$no」に代入しなさい!という処理なのです。
かなりややこしいですが、たった一行でこんな処理ができるのもPerlの魅力でしょうか?ちなみに、「@logdataの3番目の配列にあるカンマで区切られた2番目のデータを取り出す」場合には「(split(/\,/, $logdata[2]))[1]」となるのでしょう。
次に書き込まれた記事の時間の取得です。時間取得の処理は「動的ページを作る」ときに実験しているので解説は不要かと思いますが、ちょっと工夫が必要なのは29行目です。
27: # 時間の取得
28: ($sec,$min,$hour,$day,$mon,$year,$week) = localtime(time);
29: @weekname = ('日','月','火','水','木','金','土');
30: $date = sprintf("%04d年%02d月%02d日(%s) %02d:%02d:%02d",
$year+1900,$mon+1,$day,$weekname[$week],$hour,$min,$sec);
|
曜日については「localtime(time)」で得ることができますが(上記「 $week 」)、番号でしか取得できません。そこで「@weekname」という配列にあらかじめ表記したい文字を用意しておいて、その番号に対応した曜日を表示できるようにしておきます。
それを「sprint」命令で日本語として読みやすい日時表記に整理して「$date」という変数に格納しています。
これで書き込みの為に必要なデータは全てそろいました。記事番号、書込日時、名前、件名、内容を保存したい形式(ここではカンマ区切りのCSV形式)で保存しますので、下の33行目にその形式でデータを整えています($add_dataを参照ください)。
32: # 追加するデータの作成
33: $add_data = "$no,$FORM{u_name},$FORM{subject},$FORM{comment},$date\n";
34:
35: # 配列に追加
36: unshift(@logdata,$add_data);
37:
38: # ファイルの保存
39: open (OUT, ">logfile.dat") || &error("書き込みエラーが発生しました。");
40: print OUT @logdata;
41: close(OUT);
|
もし、書き込まれた順番に保存するだけなら、この「$add_data」をファイルに追記する処理でいいのですが、通常掲示板は書き込みが新しい順番(つまり、時間を遡る順番)で書き込まれています。そこで、最新の記事から順番となるように、「@logdata」の配列の一番先頭に今回のデータを追加するために36行目で「unshift」という処理をしています。
先ほど「pop()」という処理をしましたが、「pop」は配列の一番末尾のデータを削除する命令です。一方「unshift」は配列の一番先頭にデータを追加する命令です。このあたりの命令は配列処理で多様するので覚えておく必要がありそうです。
最後に処理が終了した「@logdata」をファイルに書き込んで、書き込み処理終了!ということになります。
基本的に上の処理で書き込みは正常に終了しています。が、書き込みを行った利用者に正常に終わったことを伝えたほうが親切なので、最後に「終了しました」という内容のHTMLを書き出しています。
45: # ここからHTMLでの表示
46: print <<"HTML";
47: Content-Type: text/html
48:
49: <html><head><title>シンプルBBS書込終了</title></head>
50: <body>
51: 書き込みは正常に終了しました。<br>
52: <br>
53: </body></html>
54: HTML
55:
56: exit;
|
以上が書き込みの一連の処理です。ここでは解説を割愛しましたが、エラー処理のための「 sub error(){ } 」というサブルーチンも必要となります。
このソースは下からダウンロードできます。拡張子がテキストになっているので、もし使うなら適宜変更してください。また、この情報を利用して掲示板を動かしてもぜんぜんOKなのですが、当然保証はしませんのでご了承ください。
>>ここで利用したプログラムのソースはこちら
>>ここで利用したプログラムのサンプルはこちら(書込)
>>ここで利用したプログラムのサンプルはこちら(表示)
次回は書き込まれたデータの表示です。
(2004/3/23 by あいまい)
(2004/3/23 一部修正あり)
|