投稿

前回のスクリプトで

のっけから何ですが、前回のスクリプトで、

 35: mysql_query($sql_str, $dbh)
 36:   or die('SQLエラー..'.$sql_str);

このように、MySQLに対しSQLを発行し、エラーだったら死ぬ、という処理がありました。

たった2行とはいえ、こういう処理は何度も出てくるわけですし、 どうせなら関数化してしまおうそうしよう、というわけで、 こんなの↓を書きました。今。

function mysql_query_or_die($sql_str) {
  $result = mysql_query($sql_str) or die('SQLエラー'.$sql_str);

  return $result;
}

どうせエラーのあと復帰する気がないのなら、これで十分でしょう。

ついでに、以後のスクリプトでは、 「掲示板の名前を取得する」という処理も何度か出てくるので、 こんなの↓も書きました。

function get_board_name($board_id) {
  $sql_str = sprintf("select name from board where boardid='%s'",
                     $board_id);
  $result = mysql_query_or_die($sql_str);
  if (mysql_num_rows($result) != 1) {
    die("掲示板のIDが変です。");
  }
  $row = mysql_fetch_assoc($result);
  return $row["name"];
}

これは、boardテーブルをboardidをキーに検索し、 nameフィールドをひっぱてくるという一連の処理です。

こういう部分の関数化を、 解説記事を書きながらやってるってことは、 元はべた書きだったわけで… 今回のスクリプトはその程度の姿勢で書いてたということですけど、 やっぱこんなこっちゃいかんですね。

投稿フォーム

ではいよいよ本題です。この掲示板では、投稿処理は、 以下のような画面遷移で構成されています。

最初の投稿フォームは、新規投稿または返信の形で起動されます。 新規投稿の場合のURLはこう、

http://kmaebashi.com/bbs/form.php?boardid=kmaebashibbs

返信の場合のURLはこうなります。

http://kmaebashi.com/bbs/form.php?boardid=kmaebashibbs&parent=1

このparent=1の部分で、返信対象の発言(親発言)のIDを指定しています。

では以下が、form.phpのソースコードです。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: ?>
  5: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
  6: <html lang="ja-JP">
  7: <head>
  8: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
  9: <link rel="STYLESHEET" TITLE="default" TYPE="text/css" href="./bbs.css">
 10: <?php
 11: if (!isset($_GET["boardid"])) {
 12:   die('URLが変です。');
 13: }
 14: $board_id=$_GET["boardid"];
 15: $board_name = get_board_name($board_id);
 16: 
 17: if (isset($_GET["parent"])) {
 18:   $parent = $_GET["parent"];
 19:   if (!ctype_digit($parent)) {
 20:     die("誰にレスするつもりなんだか");
 21:   }
 22:   $sql_str = sprintf("select * from message where boardid='%s' and serialid=%d",
 23:                    $board_id, $parent);
 24:   $result = mysql_query_or_die($sql_str);
 25:   if (mysql_num_rows($result) != 1) {
 26:     die("誰にレスしてるつもりなんだろう…");
 27:   }
 28: $row = mysql_fetch_assoc($result);
 29: $parent_subject=$row["subject"];
 30: $parent_message=$row["message"];
 31: }
 32: ?>
 33: <TITLE><?=$board_name?> 投稿フォーム</TITLE>
 34: <script language="JavaScript">
 35: <!--
 36:   function get_cookie(key) {
 37:     var i, index, splitted;
 38:     var sstr = key + "=";
 39:     var sstrlen = sstr.length;
 40:     splitted = document.cookie.split("; ");
 41: 
 42:     for(i = 0; i < splitted.length; i++) {
 43:       if (splitted[i].substring(0, sstrlen) == sstr) {
 44:         return unescape(splitted[i].substring(sstrlen));
 45:       }
 46:     }
 47:     return "";
 48:   }
 49:   function set_cookie(key, val) {
 50:     document.cookie =
 51:     key + "=" + escape(val) + "; expires=Wed, 01-Jan-2031 00:00:00 GMT;";
 52:   }
 53:   function set_cookies() {
 54:     set_cookie("name", document.mainForm.name.value);
 55:     set_cookie("url", document.mainForm.url.value);
 56:     set_cookie("password", document.mainForm.password.value);
 57:   }
 58: //-->
 59: </script>
 60: </head>
 61: <body>
 62: <table><tr><td align="center">
 63:   <font size="6" color="#0000ff"><?=$board_name?> 投稿フォーム</font><br>
 64:   <hr>
 65:   <CENTER>
 66:   <FONT color="red" size="4">注意!!</FONT><BR>
 67:   この掲示板では、手で改行を入れない限り改行されません。<BR>
 68:   <SMALL>(ソースを貼るときのためにPREで囲むため)</SMALL><BR>
 69:   適当な場所で改行を入れてください。<BR>
 70:   <a href="http://kmaebashi.com/bbshelp.html">ヘルプ</a>
 71:   </CENTER>
 72:   <form name="mainForm" action="preview.php" method="post">
 73:   <input type="hidden" name="boardid" value="<?=htmlspecialchars($board_id)?>">
 74: <?php
 75:   if (isset($parent)) {
 76: ?>
 77:   <input type="hidden" name="parent" value="<?=$parent?>">
 78: <?php
 79:   }
 80: ?>
 81:   <table border="1">
 82:    <tr>
 83:      <td>ハンドル名</td>
 84:      <td><input type="text" name="name" size="40"></td>
 85:    </tr>
 86:    <tr>
 87:      <td>件名</td>
 88:      <td><input type="text" name="subject"
 89: <?php
 90:   if (isset($parent)) {
 91:     if (ereg('^Re:.*$', $parent_subject)) {
 92:       $new_subject = $parent_subject;
 93:     } else {
 94:       $new_subject = "Re:" . $parent_subject;
 95:     }
 96: ?>
 97:          value="<?=$new_subject?>"
 98: <?php
 99:   }
100: ?>
101:          size="40"></td>
102:    </tr>
103:    <tr>
104:      <td>Link</td>
105:      <td><input type="text" name="url" size="40"></td>
106:    </tr>
107:  </table>
108:  <table>
109:  <tr><td>
110: <textarea name="message" cols="80" rows="20">
111: <?php
112:   if (isset($parent)) {
113: ?>
114: <?=get_parent_message($parent_message)?>
115: <?php
116:   }
117: ?>
118: </textarea>
119:  </td></tr>
120:   </table>
121:   <table><tr>
122:    <td>削除パスワード : <input type="text" name="password" size="12"></td>
123:    <td width="30"></td>
124:    <td><input type="submit" value="送信" onClick="set_cookies();">
125:        クリック!</td>
126:   </tr></table>
127:   </form>
128:  </td></tr></table></div>
129:  <script language="JavaScript">
130:  <!--
131:   document.mainForm.name.value = get_cookie("name");
132:   document.mainForm.url.value = get_cookie("url");
133:   document.mainForm.password.value = get_cookie("password");
134:  //-->
135:  </script>
136:  
137: </body>
138: </html>

31行目までは主にエラーチェックです。親発言が本当にあるかどうかチェックした上で、 もしあれば、subjectとmessageをひっぱってきています。 返信用フォームにあらかじめ表示するためです。

34〜59行目は、JavaScriptの関数定義です。 cookieを使用して、ハンドルネームやらURLやらをおぼえておくためのものです。 131〜134行目にて、cookieの値をINPUT要素に指定していますし、 submitの時点で(124行目のonClick)INPUT要素の値をcookieに保存しています。

実は40行目の「splitted = document.cookie.split("; ");」あたり、 実は実装が甘いんじゃないかという気がしていますが、 今のところ動くので放置しています。 よろしければご指摘ください > 詳しい方

3行目でrequireしているutil.phpのソースは以下の通りです。

  1: <?php
  2: function format_date($src) {
  3:   $ret = substr($src, 0, 4) . "/" . substr($src, 4, 2) . "/" . substr($src, 6, 2)
  4:            . " " . substr($src, 8, 2) . ":" . substr($src, 10, 2) . ":"
  5:            . substr($src, 12, 2);
  6: 
  7:   return $ret;
  8: }
  9: 
 10: function is_positive_number($str) {
 11:   if (!ctype_digit($str)) {
 12:     return false;
 13:   }
 14:   if ($str <= 0) {
 15:     return false;
 16:   }
 17: 
 18:   return true;
 19: }
 20: 
 21: function get_parent_message($src) {
 22:   $dest=htmlspecialchars($src);
 23:   $dest=ereg_replace('^', '&gt;' ,$dest);
 24:   $dest=ereg_replace("\n", "\n>" ,$dest);
 25:   return $dest;
 26: }
 27: 
 28: function convert_message($src) {
 29:   $dest = ereg_replace("http://[^<>[:space:]]+[[:alnum:]/]",
 30:                        "<a href=\"\\0\">\\0</a>", $src);
 31:   return $dest;
 32: }
 33: 
 34: function mysql_query_or_die($sql_str) {
 35:   $result = mysql_query($sql_str) or die('SQLエラー'.$sql_str);
 36: 
 37:   return $result;
 38: }
 39: 
 40: function get_board_name($board_id) {
 41:   $sql_str = sprintf("select name from board where boardid='%s'",
 42:                      $board_id);
 43:   $result = mysql_query_or_die($sql_str);
 44:   if (mysql_num_rows($result) != 1) {
 45:     die("掲示板のIDが変です。");
 46:   }
 47:   $row = mysql_fetch_assoc($result);
 48:   return $row["name"];
 49: }
 50: ?>

util.phpはあちこちのソースでrequireされるユーティリティ関数群なので、 まだ使用していない関数も含まれます。 form.phpでは、既に説明したmysql_query_or_die(), get_board_name()のほか、 get_parent_message()を使用しています。これは行頭に「>」を付ける関数ですね。

「送信」をクリックすると、プレビュー画面に移ります。

プレビュー画面

preview.phpのソースは以下の通りです。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: ?>
  5: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
  6: <html lang="ja-JP">
  7: <head>
  8: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
  9: <link rel="STYLESHEET" TITLE="default" TYPE="text/css" href="./bbs.css">
 10: <?php
 11: $boardid=stripslashes($_POST["boardid"]);
 12: $board_name = get_board_name($boardid);
 13: ?>
 14: <TITLE><?=$board_name?> 投稿</TITLE>
 15: </head>
 16: <body>
 17: <?php
 18: if ($_POST["name"]=="") {
 19:   die("名前の入力は必須です。");
 20: }
 21: if ($_POST["message"]=="") {
 22:   die("内容がないよう");
 23: }
 24: $name=stripslashes($_POST["name"]);
 25: if ($_POST["subject"]=="") {
 26:   $subject = "無題";
 27: } else {
 28:   $subject = stripslashes($_POST["subject"]);
 29: }
 30: $url=stripslashes($_POST["url"]);
 31: $message=stripslashes($_POST["message"]);
 32: $password=stripslashes($_POST["password"]);
 33: ?>
 34: <h1>投稿前のプレビュー</h1>
 35: <p>
 36: こんな感じで投稿されます。ちゃんと改行を入れたか等チェックしてください。
 37: </p>
 38: <p>
 39: 「投稿」ボタンをクリックすると、投稿されます。
 40: </p>
 41: <DIV class="res" style="background-color:white;">
 42:   <DIV style="margin: 10px 10px 10px 10px;">
 43:    <BR>
 44:    <DIV style="line-height:0%;">
 45:     [発言番号]
 46:     <STRONG><font size="4">
 47:       <?= htmlspecialchars($subject) ?>
 48:     </font></STRONG>
 49:     <DIV align="right"><font color="red"><u>返信</u></font></DIV>
 50:    </DIV>
 51:    <BR>
 52:    <BR>
 53:    <DIV style="line-height:0%;">
 54:      投稿者:<?= htmlspecialchars($name) ?>
 55:     <DIV align="right">YYYY/MM/DD hh:mm:ss</DIV>
 56:    </DIV>
 57:    <BR>
 58:    Link:<a href="<?=htmlspecialchars($url)?>"><?=htmlspecialchars($url)?></a>
 59:    <HR>
 60: <PRE>
 61: <?php
 62: $message2 = htmlspecialchars($message);
 63: $message2 = convert_message($message2);
 64: ?>
 65: <?=$message2?>  
 66: </PRE>
 67: </DIV>
 68: </DIV>
 69: <form action="insert.php" method="post">
 70: <input type="hidden" name="name" value="<?=htmlspecialchars($name)?>">
 71: <input type="hidden" name="subject" value="<?=htmlspecialchars($subject)?>">
 72: <input type="hidden" name="url" value="<?=htmlspecialchars($url)?>">
 73: <input type="hidden" name="message" value="<?=htmlspecialchars($message)?>">
 74: <input type="hidden" name="password" value="<?=htmlspecialchars($password)?>">
 75: <input type="hidden" name="boardid" value="<?=htmlspecialchars($boardid)?>">
 76: <?php
 77:   if (isset($_POST["parent"])) {
 78:     if (!ctype_digit($_POST["parent"])) {
 79:       die("誰にレスするつもりなんだか");
 80:     }
 81: ?>
 82:     <input type="hidden" name="parent" value="<?=$_POST["parent"]?>">
 83: <?php
 84:   }
 85: ?>
 86: <div align="center">
 87: <input type="submit" value="送信">
 88: </div>
 89: </form>
 90: </body>

この画面には、投稿フォームからPOSTでデータがやってくるのですが、 前回も説明したとおり、 うちの設定ではPOSTでやってきたデータは全てmagic quoteされているので、 片っ端からstripslashes()で元に戻し、 その上でhtmlspecialchars()でサニタイジングして表示しています。

63行目でutil.phpのconvert_message()を呼び出していますが、 ここで、URLをリンクにするという変換を行っています。

この関数は、プレビューの時だけでなく、実際に掲示板の内容を表示する際にも使います。 つまり、うちの掲示板では、データベースに格納されているメッセージは、 URLはリンクになっていませんし、ついでに言えばサニタイジングもされてません。 全ての変換は表示のタイミングで行っています。

このようにしているのは、サニタイジングやクリッカブルURLのような変換は あくまで表示上の都合であって、本来のデータの「あるべき姿」ではない、 と考えたからです。 もちろんこのへんはトレードオフで、 掲示板なんて投稿の頻度に比べて表示される頻度の方が圧倒的に高いので、 効率を考慮して、D/Bに変換済みの文字列を入れておく、 という選択肢もあると思うのですが、 ここはやっぱり富豪的に行こうということでこうしてあります。

preview.phpからinsert.phpへの値の受け渡しはhiddenで行っています(70〜75, 81行目)。

データベースへの書き込み

次はinsert.phpです。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: ?>
  5: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
  6: <html lang="ja-JP">
  7: <head>
  8: <link rel="STYLESHEET" TITLE="default" TYPE="text/css" href="./bbs.css">
  9: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
 10: <?php
 11: if (!isset($_POST["boardid"])) {
 12:   die('掲示板のIDが変です。');
 13: }
 14: $board_id=$_POST["boardid"];
 15: $board_name = get_board_name($board_id);
 16: ?>
 17: <TITLE><?=$board_name?> 投稿</TITLE>
 18: </head>
 19: <body>
 20: <?php
 21: if ($_POST["name"]=="") {
 22:   die("名前の入力は必須です。");
 23: }
 24: if ($_POST["message"]=="") {
 25:   die("内容がないよう");
 26: }
 27: $name=$_POST["name"];
 28: if ($_POST["subject"]=="") {
 29:   $subject = "無題";
 30: } else {
 31:   $subject = $_POST["subject"];
 32: }
 33: $url=$_POST["url"];
 34: $message=$_POST["message"];
 35: $password=$_POST["password"];
 36: if (isset($_POST["parent"])) {
 37:   $parent = $_POST["parent"];
 38:   if (!ctype_digit($parent)) {
 39:     die("誰にレスするつもりなんだか");
 40:   }
 41:   $sql_str = sprintf("select * from message where boardid='%s' and serialid=%d",
 42:                      $board_id, $parent);
 43:   $result = mysql_query_or_die($sql_str);
 44:   if (mysql_num_rows($result) != 1) {
 45:     die("誰にレスしてるつもりなんだろう…");
 46:   }
 47:   $row = mysql_fetch_assoc($result);
 48:   $top = $row["top"];
 49: } else {
 50:   $parent = 'null';
 51: }
 52: 
 53: mysql_query_or_die("lock tables message write");
 54: $sql_str = sprintf("select count(*)"
 55:                    . " from message where boardid='%s'",
 56:                    $board_id);
 57: $result = mysql_query_or_die($sql_str);
 58: $row = mysql_fetch_row($result);
 59: if ($row[0] == 0) {
 60:   $serial_id = 0;
 61: } else {
 62:   $serial_id = $row[0];
 63: }
 64: if (!isset($_POST["parent"])) {
 65:   $top = $serial_id;
 66: }
 67: $remote_host = getenv("REMOTE_HOST");
 68: $ip_address = getenv("REMOTE_ADDR");
 69: if($remote_host == "" || $remote_host == $ip_address){
 70:   $remote_host = gethostbyaddr($ip_address);
 71: }
 72: $user_agent = getenv("HTTP_USER_AGENT");
 73: 
 74: $sql_str = "insert into message (";
 75: $sql_str .= "boardid, serialid, name, subject, url, message, password, "
 76:             . "parent, top, ipaddress, remotehost, useragent"
 77:             . ") values (";
 78: $sql_str .= sprintf("'%s', %d, '%s', '%s', '%s', "
 79:                     . "'%s', '%s', %s, %s, "
 80:                     . "'%s', '%s', '%s')",
 81:                     $board_id, $serial_id, $name, $subject, $url,
 82:                     $message, $password, $parent, $top,
 83:                     $ip_address, $remote_host, $user_agent);
 84: mysql_query_or_die($sql_str);
 85: mysql_query_or_die("unlock tables");
 86: ?>
 87: <div align="center">
 88: <p>
 89: 投稿が成功しました。
 90: </p>
 91: <p>
 92: <a href="./list.php?boardid=<?=$board_id?>">
 93: 一覧表示に戻る</a>
 94: </div>
 95: </body>

51行目あたりまで、preview.phpで見たようなエラーチェックが並んでいます。 二重にチェックすることになるわけですが、 hidden要素はクライアント側で書き換え可能なので、 こうしたチェックはセキュリティのためにも必要です。 だったらちゃんとまとめて関数化しておけよ、と言われたらその通りです。 返す言葉もありません。 そのうちなんとかしよう…

ところで、投稿の際は、発言番号の採番をしなければなりません。

ひとつの掲示板しかサポートしないのなら、 messageテーブルのserialidをauto incrementにでもしておけば一発ですが、 この掲示板では、複数の掲示板を作ることができますから、 発言番号は各掲示板内でユニークということになります。 よって、その掲示板の中での過去の発言番号の最大値を取得し、 自力で採番する必要があります。

ここで排他制御の必要が出てきます。 AさんとBさんがきわどいタイミングで同時に投稿すると、 以下のようなことが発生する可能性があります。

  1. Aさんが投稿。この時点での発言番号の最大値は10。
  2. Bさんが投稿。まだAさんの投稿が終わっていないので、 この時点での発言番号の最大値も10。
  3. Aさんの投稿を11番としてD/Bに書き込み。
  4. Bさんの投稿も、11番としてD/Bに書き込まれてしまう。

そこで、53行目でテーブルに対してロックをかけています。 次にロックを取得しに来た人は、 前の人がロックを外すまで(85行目のアンロック)待たされます。 これにより、発言番号の採番から書き込みまでの処理が atomicであることが保証されるわけです。


ひとつ上のページに戻る | ひとつ前のページ | ひとつ後のページ | トップページに戻る
mailto:PXU00211@nifty.ne.jp

このページに対してご意見・ご質問・ご感想等をいただいた場合、 公開することがあります。