印刷用表示へ切り替え 通常表示へ切り替え 更新履歴を表示 更新履歴を隠す
DownloadLinux Very Simple Shoutbox 1.0 解説

簡単ゲストブックプログラムの詳細について解説する。解説といっても実は覚書。書いておかないと忘れてしまうからだ。

■ ■ ■

前回の記事では Picasa にコメント欄をつける方法を紹介したが(nlog(n): Picasa の Web エクスポートに PukiWiki 風のコメント欄をつける),このスクリプトは Picasa だけではなく一般のページに使うことができる。PHP が動作しているサーバであれば設置可能な1行コメント用スクリプトである。

Very Simple Shoutbox in PHP v1.0 のプログラムの詳細について見ていくことにする。

このスクリプトは次の5つの部分で構成されている。

  1. パラメータ設定部
  2. メッセージ書込み部
  3. メッセージ表示部
  4. 投稿用フォーム表示部
  5. サニタイジング関数部

それぞれについて個別に見ていくことにする。

パラメータ設定部

$self = basename(__FILE__);
// $file = "$self.txt"; // message file for general use
$file = "<%itemName%>.txt"; // message file for Picasa2 Web template
$maxlen = "70";
$yournamelabel = "&#12362;&#21517;&#21069;:"; //お名前:
$submitlabel = "&#12467;&#12513;&#12531;&#12488;&#12398;&#25407;&#20837;"; //コメントの挿入
$week = array("&#26085;", "&#26376;", "&#28779;","&#27700;", "&#26408;", "&#37329;", "&#22303;"); //日月火水木金土

「//」から行末まではコメントとして解釈される。「/*」と「*/」で挟んでもコメントになる。

「basename(__FILE__);」で,このスクリプトが設置されている自身のファイル名が取得できる。ファイル名は $self 変数に代入され,コメント用データファイル名やフォームの送信先に使われる。

// $file = "$self.txt"; // message file for general use
$file = "<%itemName%>.txt"; // message file for Picasa2 Web template

これはコメント用データのファイル名の指定。Picasa のテンプレートに使うときは下のファイル名を,それ以外の場合は,Picasa 用のファイル名をコメントアウトして,上の一般用のファイル名を使う。このスクリプトを設置したファイル名が「guestbook.php」だとすると,データファイル名は「guestbook.php.txt」になる。

当初は,Picasa 用ファイル名指定はなかった。一般用でも Picasa に使うことはできる。Picasa がエクスポートする各画像用ファイルは「target0.html」や「target1.html」など,画像のファイル名に関わらず連番になる。このファイル名にデータファイルを結びつけてしまうと,画像を増やしてエクスポートし直した場合に,違う画像のコメントが表示されてしまう可能性があるからである。そこで,画像のファイル名「<%itemName%>」に拡張子「txt」を追加したファイルをデータファイルにすることにした。これなら画像が増えてもメッセージがずれる心配がない。

$maxlen = "70";

$maxlen は投稿できるメッセージの長さの上限を設定している。投稿者の名前の長さの上限も,同時にこれで設定している。この上限値は数値として使っていないので,ダブルクォートで囲んである。ダブルクォートの代わりにシングルクォートでもよい。数値とするならクォーテーションマークを外す。

$yournamelabel = "&#12362;&#21517;&#21069;:"; //お名前:
$submitlabel = "&#12467;&#12513;&#12531;&#12488;&#12398;&#25407;&#20837;"; //コメントの挿入
$week = array("&#26085;", "&#26376;", "&#28779;","&#27700;", "&#26408;", "&#37329;", "&#22303;"); //日月火水木金土

表示に使う日本語は,文字化けを起こさないように数値文字参照で指定した。PHP の内部コードと表示に使うコードが異なる場合があるからである。$yournamelabel と $submitlabel はフォームを表示するときに使っている。$week の曜日は,データファイルに書き込まれる。PHP の内部コードと関係があるのは $week だけである。

このセクションで設定した値は,他の箇所で変更されることはない。変数ではなく,定数にしてしまった方がいいのかも知れない。

メッセージ書込み部

foreach ($_POST as $key => $value) {
  if ($key == "submit") {
    $name = $_POST['name'];
    $name = clean($name, $maxlen);
    $msg = $_POST['msg'];
    $msg = clean($msg, $maxlen);
    $date = date("Y-m-d") . ' (' . $week[date("w")] . ') ' . date("H:i:s");
    $handle = fopen($file, "a+") or die("Can't write message to file: $file");
    if ($name) fwrite($handle, "$date\t$name\t$msg\n");
    fclose($handle);
  }
}

メッセージ書込み部は,「コメント挿入」ボタンをクリックしたときだけ実行される。クリックされたかどうかは $_POST 変数で判断することができる。$_POST 変数 は PHP 4.1.0 で導入された(定義済の変数)。コメント挿入ボタンは「submit」という名前に設定しているので(後述のフォーム部),ボタンがクリックされたときは $_POST['submit'] に値が入ることになる。

foreach ($_POST as $key => $value) {
  if ($key == "submit") { ... }
  ...
}

の部分は,

if ($_POST['submit']) {
  ...
}

と書いても動作する。ただし,Apache のエラーログに次のようなエラーが残る。

[error] PHP Notice:  Undefined index:  submit in vsshout10.php on line 11

submit という添え字が定義されていないというメッセージである。このスクリプトは,ボタンをクリックしたときとしないときの両方で実行される。ボタンがクリックされない場合は $_POST['submit'] が定義されないからである。

投稿者の名前と投稿メッセージについては,サニタイジングを行っている(後述)。

if ($name) fwrite($handle, "$date\t$name\t$msg\n");

この部分でメッセージをファイルに書き込んでいる。$name に値が入っているときだけに限定しているのは,「いたずら防止」ではなく「うっかり防止」が目的。日付,名前,メッセージはタブ区切りとした。名前にタブが含まれていると困るので,後述のサニタイズによりタブはスペースに変換している。

メッセージ表示部

if ($handle = @fopen($file, "r")) {
  echo "<ul>";
  while (!feof($handle)) {
    $line = fgets($handle, 1024);
    if ($line) {
      list($date, $name, $msg) = split("\t", $line);
      echo "<li>$name -- $msg $date</li>";
    }
  }
  echo "</ul>";
  fclose($handle);
}

ファイルを開くのには fopen 関数を使う。上述の fopen では,モードを "a+" として開いたので,「読み込み・書き出し用」になった。ファイルが存在する場合は追記用,ファイルが存在しない場合は新規作成となった。ここではファイルを読むだけなので,モードを "r" として開いている。

$handle = @fopen($file, "r")

fopen の前に @ をつけてあるのは,ファイルが存在しない場合のエラーを抑制するためである(@)。

1行ずつ読み込むようにするには fgets 関数を使うのが簡単である。

$line = fgets($handle, 1024);

ただし,PHP: fgets - Manual には次のような注意がある。

陥りやすい罠:
C言語のfgetsの動作に慣れている人は、EOFを返す条件の違いについて 注意する必要があります。

C言語の fgets 関数は,EOF を検出すると NULL を返す。EOF を返す訳ではないので,どこまで違うかよく分からないが,PHP の fgets を使う場合に気をつけておいた方がよいことはある。ファイルの最後の行の行末が改行で終わっている場合,fgets はその行の改行までを読み込み,EOF を残す。そして,もう一度 fgets が呼ばれたときに EOF を読み込んで $line に空文字列をセットする。つまり,EOF を独立した1行の空行のように扱ってしまうということである。

list($date, $name, $msg) = split("\t", $line);

fgets で読み込んだ行を,タブを区切り文字にして $date, $name, $msg に代入しているのだが,ここで上述の EOF のみの場合に問題が起こる。スクリプト自体は正常に実行される。しかし,Apache のエラーログに次のメッセージが出力されてしまう。

[error] PHP Notice:  Undefined offset:  2 in vsshout10.php on line 29
[error] PHP Notice:  Undefined offset:  1 in vsshout10.php on line 29

これは,split によってできた配列の,1と2の添え字部分が定義されていないことを意味している。配列の添え字は0から始まる。EOF を読み込んだ場合,$line の添え字0は定義され,空文字列が代入されるが,その後の文字列がないために添え字1と2は定義されないからである。

fgets 関数の第2引数は PHP 4.2.0 からオプションになった(つまり指定してもしなくてもよい)。4.2.0 より前のバージョンの場合,エラーを出さずに止まっているように見えるとのことなので(閑古鳥 -> 徒然草 -> 2002年12月),明示的に 1024 を指定している。指定しない場合も 1024 になる(PHP: fgets - Manual)。

投稿用フォーム表示部

echo <<<HERE_DOCUMENT
<form method="post" action="$self">
$yournamelabel
<input type="text" name="name" size="15" value="" />
<input type="text" name="msg" size="70" value="" />
<input type="submit" name="submit" value="$submitlabel" />
</form>
HERE_DOCUMENT;

ここでは,ヒアドキュメントを使って投稿用フォームを表示している(PHP: echo - Manual)。Perl のヒアドキュメントと似ているが,セミコロンの位置や「<」の数が違うので注意が必要である(基本的な構文 - perl使いのPHP)。

「コメントの挿入」というボタン (type="submit") に,「submit」という名前をつけている(name="submit") のは,書き手の趣味である。

一番初めに求めた $self を使って,自分自身に POST している。

サニタイジング関数部

function clean($name, $maxlen) {
  $name = ereg_replace("[[:space:]]", " ", $name);
  $name = mb_strimwidth($name, 0, $maxlen);
  $name = htmlspecialchars($name, ENT_QUOTES);
  return $name;
}

POST された投稿者の名前とメッセージは,サニタイジング用関数で無害化する。まず,タブや改行をスペースに変換する。メッセージ用データファイルは区切り文字をタブにしているからである。

mb_strimwidth() はマルチバイト対応の関数で,指定した文字数で文字列をカットする(PHP: mb_strimwidth - Manual)。マルチバイト対応でない関数 (substr 等) を使うと,2バイト日本語文字が文字の中間で切られる可能性があり,文字化けの原因になる。

最後に htmlspecialchars() で,HTML タグで使う文字を実体参照文字に変換している。HTML タグが入力されても,変なことが起こらないようにしている。htmlspecialchars() 関数は,前の mb_strimwidth() より後ろでなければならない。前に使うと,mt_strimwidth() が実体参照の途中で切ってしまうことがあるからである。

clean() 関数の仮引数名には,呼び出し側の実引数名と同じ名前をつかっているが,スコープが違うので別のものである。同じ名前にしているのは書き手の趣味である。

2007年4月13日追記:
サニタイジングをしない形に書き直しました (nlog(n): Very Simple Shoutbox 1.1)。

Posted by n at 2006-04-30 22:11 | Edit | Comments (0) | Trackback(0)
Trackbacks

  • 「手違いで複数トラックバックを送ってしまった!」という場合でも気にしないでください (重複分はこちらで勝手に削除させていただきます)
  • タイムアウトエラーは,こちらのサーバの処理能力不足が原因です (詳細は トラックバック送信時のエラー をご覧ください)
  • トラックバックする記事には,この記事へのリンクを含めてください(詳細は 迷惑トラックバック対策 をご覧ください)
Comments
Post a comment
  • 電子メールアドレスは必須ですが,表示されません (気になる場合は「メールアドレスのような」文字列でもOKです)
  • URL を入力した場合はリンクが張られます
  • コメント欄内ではタグは使えません
  • コメント欄内に URL を記入した場合は自動的にリンクに変換されます
  • コメント欄内の改行はそのまま改行となります
  • 「Confirmation Code」に表示されている数字を入力してください (迷惑コメント対策です)


(必須, 表示されます)


(必須, 表示されません)


(任意, リンクされます)


Confirmation Code (必須)


Remember info (R)?