削除

仕様を考える

削除機能を付けるにあたって、いろいろ仕様を考えました。

削除機能には、投稿者削除と管理者削除がある

削除には、投稿者が、しくじった投稿を自分で削除するケースと、 管理者が、広告やら荒らし投稿やらを管理者権限で削除するケースがあります。

「管理者」とは、「その板の管理者」である

最初はあまりちゃんと考えてなかったのですが、 この掲示板では複数の板を管理できますから、 「管理者」とひとことで言っても 「掲示板全体のシステムを管理する管理者」なのか 「個々の板を管理する管理者」なのかが不明です。

「複数の板が使える」という機能の目的によると思うのですが、 たとえば、うちのWebページで、話題ごとにいくつもの板を立てる、という使い方なら、 板がいくつあろうと管理者はひとりでしょう。

しかし、「友人知人に板を貸す」という使い方も考えるとなると、 管理者は板ごとに必要だということになります。 今回はこちらの、「管理者は板ごとに存在する」という仕様を採用しました。

こうなると、「全ての板を管理するスーパー管理者」というのも必要な気がしますが、 それは、データベースを直接SQLで触ればいいか、ということで、 現状では作ってません。

投稿者削除はともかく、管理者削除ははっきり削除の形跡が残るべきである

このへんは考え方の問題ですが、掲示板の管理者は、 「誰の投稿でも削除できる」という点において、 一般の利用者よりも強い立場にあると言えます。

管理者が、気に喰わない投稿を片っ端から削除するような掲示板もありますが (1ch.tvとか:-p)、 そういうのがいいとは私にはあんまり思えませんので、 管理者の横暴を牽制するためにも、削除したらその痕跡ぐらいは残すべきかと思います。

まあ、広告が大量に書き込まれる掲示板だとそれも困りますが、 ウチのような独自仕様の掲示板なら、そうそう広告が書き込まれることもないと思います。

ウチの旧掲示板はOTDのものを借りていたのですが、 OTDは掲示板のURLが連番(たぶん)で指定されるので※1、 広告書き込まれ放題ですね。困ったものです。

管理者削除では「代替メッセージ」を表示したい

どうせ削除した際に何らかの痕跡を残すなら、 そこには、管理者がなぜその発言を消したのか、その理由を入れるべきだと思います。

んで、これを実現するにあたって、その発言内容自体を書き換えてしまうと、 問題の発言はこの世から永久に消えてしまうことになり、 クレームを出したり、万一裁判沙汰とかになった場合などに困ることになります。 テーブル定義の段階で 「altermessage」というフィールドを作っておきましたが、 これはそういう使い方を想定していました。

画面上の仕様について

削除機能といえば、

などの仕様が考えられます。

この中で、発言番号を使う方法は面倒なので嫌ですし、 Cookieを使う方法は、投稿したPC以外からは削除できないので、 不便なことも(まれでしょうが)ありそうです。 というわけで、オーソドックスですが現状の仕様になりました。

管理者削除ですが、掲示板によっては、管理者専用の画面があって、 その画面で、一覧から投稿を選択し、削除するタイプのものがあります。

しかし、現状の掲示板は、目的の投稿が簡単に探せるよう、 インデックス表示やスレッド表示などの機能を付けているわけで、 管理者削除の際もどうせならこれを使いたいところです。

というわけで、投稿ごとの「削除」をクリックした次の確認画面に 「管理者削除」のリンクをつけることにしました。 このリンクをクリックすると、管理者用の削除画面に移動します。

テーブル変更

テーブル定義の回では、 ある程度先を見越したテーブル定義をしようとしたつもりでしたが、 やっぱり先が見えていなかったところもあったようで、 結局テーブル定義を変更しています。

まずはboardテーブルから。追加したフィールドは色付けしました。

boardidvarchar(32)このテーブルのキー。英数字。 URLにおいて「boardid=kmaebashibbs」という形式で指定しているのがこれ。
namevarchar(128)板名(日本語)
adminnamevarchar(64)管理者名(日本語)
adminpasswordvarchar(64)管理者パスワード
saltvarchar(64)管理者パスワードのsalt
defaultrangeinteger日付順表示のときの1ページ表示数
defaultrangeindexintegerインデックス表示のときの1ページ表示数
defaultrangethreadintegerスレッド表示のときの1ページスレッド数
homepagevarchar(128)開設者Webページ
cssvarchar(64)CSSのパス
messagetextこの板の紹介文字列

管理者を板ごとにしたため、管理者の名前とパスワードのフィールドが増えています。 管理者名は、入力させるだけで使っていません。 「salt」については後述します。

また、messageフィールドを追加しました。 このフィールドにはHTMLを格納することができ、 その内容が掲示板の上部に表示されます。 その板の説明を書くために使います。

以前はshowmessagephpというフィールドがありましたが、 どうせ使わなさそうなので削除しました。

次はmessageテーブルです。

serialidintegerキー。掲示板で投稿の左上に表示される連番。
boardidvarchar(32)キー。boardテーブルのboardidを指す。
posteddatetimestamp投稿日付
namevarchar(64)投稿者
mailaddressvarchar(64)メールアドレス(使ってない)
urlvarchar(64)URL
subjectvarchar(128)件名
messagetext投稿内容
altermessagetext強制削除とかした時の代替メッセージ
passwordvarchar(64)パスワード
saltvarchar(64)パスワードのsalt
preformattedbool<PRE>で囲むかどうか(使ってない)
deletedbool削除フラグ
admindeletedbool管理者削除フラグ
parentinteger親メッセージ
topintegerスレッドのトップ
ipaddressvarchar(32)投稿者IPアドレス
remotehostvarchar(64)投稿者リモートホスト名
useragentvarchar(64)投稿者User Agent

管理者削除と投稿者削除を区別するため、admindeletedを追加しました。

いやその、別にこれを追加しなくても、 altermessageが使われるのは管理者削除の時だけなので、 altermessageが非NULLであれば管理者削除に決まっているわけですが、 そういう「明確にテーブル構造に表れない約束事」に頼るのはよろしくない、 ということで、フィールドを追加しました。 …が、deletedとadmindeletedが同時に立つことはありえないのですが、 それが明確にテーブル構造に表れないのでどっちもどっちという気もします。

また、こちらにも「salt」というフィールドが追加されていますが、 これも後述します。

削除系ソース

ではソースについて。

まずは、「この投稿を削除」をクリックすると最初に現れる画面delete.phpです。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: 
  5: if (!isset($_GET["boardid"])) {
  6:   die('掲示板のIDが変です。');
  7: }
  8: $board_id=$_GET["boardid"];
  9: ?>
 10: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 11: <html lang="ja-JP">
 12: <head>
 13: <link rel="STYLESHEET" TITLE="default" TYPE="text/css"
 14:   href="<?=get_css($board_id)?>">
 15: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
 16: <?php
 17: $board_name = get_board_name($board_id);
 18: ?>
 19: <TITLE><?=$board_name?> 削除</TITLE>
 20: </head>
 21: <body>
 22: <?php
 23: if (!isset($_GET["serialid"])) {
 24:   die("だれもかれも消えちまえ");
 25: }
 26: $serialid = $_GET["serialid"];
 27: if (!ctype_digit($serialid)) {
 28:   die("な、かんたんだろ。");
 29: }
 30: $sql_str = sprintf("select * from message where boardid='%s' and serialid=%d",
 31:                    $board_id, $serialid);
 32: $result = mysql_query_or_die($sql_str);
 33: if (mysql_num_rows($result) != 1) {
 34:   die("すみごこちのいい世界にしようじゃないか。");
 35: }
 36: $row = mysql_fetch_assoc($result);
 37: if ($row["deleted"] || $row["admindeleted"]) {
 38:   die("既に削除されています。");
 39: }
 40: ?>
 41: <p>以下のメッセージを削除します。</p>
 42: <?php
 43: $subject = htmlspecialchars($row["subject"]);
 44: $name = htmlspecialchars($row["name"]);
 45: $url = htmlspecialchars($row["url"]);
 46: $message = htmlspecialchars($row["message"]);
 47: $date = format_date($row["posteddate"]);
 48: include 'plain.php';
 49: ?>
 50: <center>
 51: <p>
 52: <form action="dodelete.php" method="post">
 53: パスワード:
 54: <input type="text" name="password" size="12">
 55: <input type="submit" value="削除">
 56: <input type="hidden" name="boardid" value="<?=$board_id?>">
 57: <input type="hidden" name="serialid" value="<?=$serialid?>">
 58: </p>
 59: <p>
 60: <a href="./admindelete.php?boardid=<?=$board_id?>&amp;serialid=<?=$serialid?>">管理者削除</a>
 61: </p>
 62: </center>
 63: </form>
 64: </body>

40行目までがエラーチェック。 どうも得体の知れないエラーメッセージが並んでますが、 元ネタは… いやアレはやっぱり名作ですよ。 もっとも、「悪魔のパスポート」はアレを超える名作だと思いますが。

馬鹿話はさておき。

残りの部分は、まあ見ればわかるでしょうが、 ユーザ権限の削除の場合は、板ID、投稿の番号、パスワードをPOSTで送信、 管理者権限の削除の場合は板IDと投稿の番号を GETのパスワードにくっつけて次の画面に遷移しています。

ユーザ権限の削除の場合は、下のPHP(dodelete.php)が動きます。 管理者権限削除については後ほど。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: 
  5: if (!isset($_POST["boardid"])) {
  6:   die('掲示板のIDが変です。');
  7: }
  8: $board_id=$_POST["boardid"];
  9: ?>
 10: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 11: <html lang="ja-JP">
 12: <head>
 13: <link rel="STYLESHEET" TITLE="default" TYPE="text/css"
 14:   href="<?=get_css($board_id)?>">
 15: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
 16: <?php
 17: $board_name = get_board_name($board_id);
 18: ?>
 19: <TITLE><?=$board_name?> 削除</TITLE>
 20: </head>
 21: <body>
 22: <?php
 23: if (!isset($_POST["password"])) {
 24:   die('パスワードを入れてください。');
 25: }
 26: $password=$_POST["password"];
 27: 
 28: if (!isset($_POST["serialid"])) {
 29:   die('じゃまものは消してしまえ。');
 30: }
 31: $serial_id = $_POST["serialid"];
 32: if (!ctype_digit($serial_id)) {
 33:   die("ぼくはどくさい者だ、ばんざい!");
 34: }
 35: $sql_str = sprintf("select password, salt from message where boardid='%s' "
 36:                    . "and serialid=%d",
 37:                    $board_id, $serial_id);
 38: $result = mysql_query_or_die($sql_str);
 39: $row = mysql_fetch_assoc($result);
 40: $db_password=$row["password"];
 41: $salt=$row["salt"];
 42: if ($db_password == hash_user_password("", $salt)) {
 43:   die("パスワードが設定されていないので削除できません。");
 44: }
 45: if (hash_user_password($password, $salt) != $db_password) {
 46:   die("パスワードが違います。");
 47: }
 48: $sql_str = sprintf("update message set deleted=1 where boardid='%s' "
 49:                    . "and serialid=%d",
 50:                    $board_id, $serial_id);
 51: mysql_query_or_die($sql_str);
 52: ?>
 53: <p>
 54: メッセージを削除しました。
 55: </p>
 56: <center>
 57: <p>
 58: <a href="./list.php?boardid=<?=$board_id?>">
 59: 一覧表示に戻る</a>
 60: </p>
 61: </center>
 62: </body>
 63: </html>

47行目まで、パスワードチェックを含めたエラーチェックになっています (メッセージは例のごとく)。 その後、48行目でSQLを組み立てて、削除しています。

ところで、45行目でパスワードチェックを行っていますが、

 45: if (hash_user_password($password, $salt) != $db_password) {
 46:   die("パスワードが違います。");
 47: }

ここでhash_user_password()という関数を呼び出し、その戻り値をD/B 中に格納されている(つまり投稿時に設定された)パスワードと比較しています。 hash_user_password()に渡しているのは、 削除しようとした人が入力したパスワードと、D/B中に格納されていたsaltです。 41行目で、レコードからsaltを取得していますが、 saltフィールドには、投稿の時点で、ランダムな文字列が設定されています。

では、hash_user_password()関数はどうなっているかというと、 これはutil.phpに定義されていて、こんな感じです。

 54: function hash_user_password($src, $salt) {
 55:   $dest = md5("秘密" . $src . $salt);
 56: 
 57:   return $dest;
 58: }

ここで、md5()という関数を呼び出しています。 この関数の説明はこちら にありますが、簡単に言えば、何らかの文字列を与えると、 32文字の16進文字列を返すというものです。 たとえば「hoge」を与えると、以下の文字列を返します。

ea703e7aa1efda0064eaa507d9e8ab7e

ここで重要なのは、md5()は「hoge」を与えれば必ず 「ea703e7aa1efda0064eaa507d9e8ab7e」を返しますが、 「ea703e7aa1efda0064eaa507d9e8ab7e」という文字列から元の 「hoge」を推測するのは極めて困難だ、ということです。 MD5というのはそういうアルゴリズムです。

この掲示板では、投稿時にユーザが入力したパスワードを、md5()で変換して データベースに格納ています。 削除の際も、入力されたパスワードをmd5()変換し、 md5()変換された文字列同士で比較しているわけです。

さて、なぜこのようなことをするかですが。

皆さん、ネットのあちこちで要求されるパスワード、それぞれ別々のものを使ってます? 結構同じパスワードをあちこちで使いまわしている人が多いんじゃないでしょうか。

もちろん、ネットバンキングなど、直接金銭と関わるところのパスワードと、 こんな個人運営の掲示板とのパスワードを同じにすべきではないですが、 同じにしてしまう人も中にはいるでしょう。 そんなパスワード、私としては知ってしまっても困るわけですが、 D/B中にはMD5変換したパスワードしか持たないようにすれば、 私はそんなのを知らずにいられるわけです。

また、こうしておくことで、掲示板を運営しているサーバが万一クラックされたり、 レンタルサーバ業者がD/B情報を漏洩させたりしたときにも、 投稿者のパスワードを守ることができます。

なお、いくらMD5アルゴリズムが逆方向の推測が困難だといっても、 辞書攻撃や総当り攻撃により破られる可能性はあります。 そこで、上のソースでは、パスワードに対し秘密の文字列(赤字部分)を連結しています。 もちろん実際のソースでは、ここには「秘密」という文字列ではなく、 推測されにくいでたらめな文字列が書かれています。 こうしておけば、そのでたらめな文字列がばれない限り、 パスワードそのものを推測することはできません。

さらに、投稿ごとに異なるランダムな文字列(salt)を連結することで、 同じパスワードに対しても、D/Bにはそれぞれ異なる文字列が格納されることになります。 こうしておけば、管理者にも 「誰と誰は同じパスワードを使っている」ということがわからなくなりますし、 事前に辞書を作成する方法によるクラックに強くなります。

追記:最初の公開時には、saltは使っていませんでした。
掲示板の方でツッコミが入り、追加しました。 saltに関する議論は こちら)をどうぞ。

現在、saltは、insert.phpの中で以下のコードにより生成しています。

 73: $salt = dechex(mt_rand());

管理者削除系ソース

次は管理者権限削除画面(admindelete.php)です。 この画面で、代替メッセージの入力ができます。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: 
  5: if (!isset($_GET["boardid"])) {
  6:   die('URLが変です。');
  7: }
  8: $board_id=$_GET["boardid"];
  9: ?>
 10: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 11: <html lang="ja-JP">
 12: <head>
 13: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
 14: <link rel="STYLESHEET" TITLE="default" TYPE="text/css"
 15:   href="<?=get_css($board_id)?>">
 16: <?php
 17: $board_name = get_board_name($board_id);
 18: 
 19: if (!isset($_GET["serialid"])) {
 20:   die('削除すべき投稿がありません。');
 21: }
 22: $serialid = $_GET["serialid"];
 23: ?>
 24: <TITLE><?=$board_name?> 管理者削除</TITLE>
 25: </head>
 26: <body>
 27: <h1><?=$board_name?> 管理者削除</h1>
 28: <?php
 29: $sql_str = sprintf("select * from message where boardid='%s' and serialid=%d",
 30:                    $board_id, $serialid);
 31: $result = mysql_query_or_die($sql_str);
 32: if (mysql_num_rows($result) != 1) {
 33:   die('削除すべき投稿が見つかりません。');
 34: }
 35: $row = mysql_fetch_assoc($result);
 36: if ($row["deleted"] || $row["admindeleted"]) {
 37:   die("既に削除されています。");
 38: }
 39: $subject = htmlspecialchars($row["subject"]);
 40: $name = htmlspecialchars($row["name"]);
 41: $url = htmlspecialchars($row["url"]);
 42: $message = htmlspecialchars($row["message"]);
 43: $date = format_date($row["posteddate"]);
 44: include './plain.php';
 45: ?>
 46: <form action="./doadmindelete.php" method="post">
 47: <input type="hidden" name="boardid" value="<?=htmlspecialchars($board_id)?>">
 48: <input type="hidden" name="serialid" value="<?=$serialid?>">
 49: <div align="center">
 50: <table>
 51: <tr><td>代替メッセージ</td></tr>
 52: <tr><td>
 53: <textarea name="altermessage" cols="80" rows="20">
 54: </textarea>
 55: </table>
 56: </tr></td>
 57: <input type="text" name="password" size="12">
 58: <input type="submit" value="削除" onClick="set_cookies();">
 59: </div>
 60: </body>
 61: </html>

…とりたてて言うこともなさそうなので次。

上の画面で、管理者パスワードを入力し「削除」をクリックすると、 下のdoadmindelete.phpが実行されます。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: if (!isset($_POST["boardid"])) {
  5:   die('掲示板のIDが変です。');
  6: }
  7: $board_id=$_POST["boardid"];
  8: ?>
  9: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 10: <html lang="ja-JP">
 11: <head>
 12: <link rel="STYLESHEET" TITLE="default" TYPE="text/css"
 13:   href="<?=get_css($board_id)?>">
 14: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
 15: <?php
 16: $board_name = get_board_name($board_id);
 17: ?>
 18: <TITLE><?=$board_name?> 管理者削除</TITLE>
 19: </head>
 20: <body>
 21: <?php
 22: if (!isset($_POST["password"])) {
 23:   die('パスワードを入れてください。');
 24: }
 25: $password=$_POST["password"];
 26: 
 27: if (!isset($_POST["serialid"])) {
 28:   die('じゃまものは消してしまえ。');
 29: }
 30: $serial_id = $_POST["serialid"];
 31: if (!ctype_digit($serial_id)) {
 32:   die("ぼくはどくさい者だ、ばんざい!");
 33: }
 34: if (!isset($_POST["altermessage"])) {
 35:   die('何をしたいんだいったい。');
 36: }
 37: $altermessage = $_POST["altermessage"];
 38: 
 39: $sql_str = sprintf("select adminpassword, salt from board where boardid='%s'",
 40:                    $board_id);
 41: $result = mysql_query_or_die($sql_str);
 42: $row = mysql_fetch_assoc($result);
 43: $db_password = $row["adminpassword"];
 44: $salt = $row["salt"];
 45: if ($db_password == hash_admin_password("", $salt)) {
 46:   die("パスワードが設定されていないので削除できません。");
 47: }
 48: if (hash_admin_password($password, $salt) != $db_password) {
 49:   die("パスワードが違います。");
 50: }
 51: $sql_str = sprintf("update message set admindeleted=1, altermessage='%s' "
 52:                    . "where boardid='%s' and serialid=%d",
 53:                    $altermessage, $board_id, $serial_id);
 54: mysql_query_or_die($sql_str);
 55: ?>
 56: <p>
 57: メッセージを削除しました。
 58: </p>
 59: <center>
 60: <p>
 61: <a href="./list.php?boardid=<?=$board_id?>">
 62: 一覧表示に戻る</a>
 63: </p>
 64: </center>
 65: </body>
 66: </html>

これまた見ての通りです。

投稿者削除では、パスワードをMD5化するのに hash_user_password()を呼んでいましたが、 管理者削除ではhash_user_password()を呼んでいます。 これは秘密の文字列が違うだけで、やってることは同じです。

表示系ソース

さて、削除機能を付けたのはいいですが、 やってることはdeletedまたはadmindeletedのフラグを立てているだけなので、 表示側でそれを解釈しなければ、元通り同じ投稿が表示されてしまいます。

というわけでまずは修正版のlist.phpから。

  1: <?php
  2: require 'connect_db.php';
  3: require 'util.php';
  4: if (!isset($_GET["boardid"])) {
  5:   die('URLが変です。');
  6: }
  7: $board_id=$_GET["boardid"];
  8: ?>
  9: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 10: <html lang="ja-JP">
 11: <head>
 12: <meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
 13: <link rel="STYLESHEET" TITLE="default" TYPE="text/css"
 14:   href="<?=get_css($board_id)?>">
 15: <?php
 16: if (!isset($_GET["mode"])) {
 17:   $mode = 'plain';
 18: } else if ($_GET["mode"] == 'index') {
 19:   $mode = 'index';
 20: } else if ($_GET["mode"] == 'plain') {
 21:   $mode = 'plain';
 22: } else {
 23:   die("modeが変です。");
 24: }
 25: if (isset($_GET["thread"])) {
 26:   $thread = $_GET["thread"];
 27:   if (!ctype_digit($thread)) {
 28:     die("threadが変です。");
 29:   }
 30: }
 31: $sql_str = sprintf("select * from board where boardid='%s'",
 32:                    $board_id);
 33: $result = mysql_query($sql_str) or die('SQLエラー'.$sql_str);
 34: if (mysql_num_rows($result) != 1) {
 35:   die("掲示板のIDが変です。");
 36: }
 37: $row = mysql_fetch_assoc($result);
 38: $board_name=$row["name"];
 39: $board_message=$row["message"];
 40: $homepage=$row["homepage"];
 41: $defaultrange=$row["defaultrange"];
 42: $defaultrangeindex=$row["defaultrangeindex"];
 43: 
 44: if (!isset($thread)) {
 45:   # 通常表示ではfromを使う
 46:   if (isset($_GET["from"])) {
 47:     $from = $_GET["from"];
 48:     if (!ctype_digit($from)) {
 49:       die("fromが変です。");
 50:     }
 51:   }
 52: } else {
 53:   # スレッド一覧表示ではoffsetを使う
 54:   if (isset($_GET["offset"])) {
 55:     $offset = $_GET["offset"];
 56:     if (!ctype_digit($offset)) {
 57:       die("offsetが変です。");
 58:     }
 59:   }
 60: }
 61: if (!isset($_GET["range"])) {
 62:   if ($mode == 'plain') {
 63:     $range = $defaultrange;
 64:   } else if ($mode == 'index') {
 65:     $range = $defaultrangeindex;
 66:   }
 67: } else {
 68:   $range = $_GET["range"];
 69:   if (!is_positive_number($range)) {
 70:     die("rangeが変です。");
 71:   }
 72: }
 73: $sql_str = sprintf("select * from message where boardid='%s' ",
 74:                    $board_id);
 75: if (isset($from)) {
 76:   $sql_str .= sprintf("and serialid <= %d ", $from);
 77: }
 78: if (!isset($thread)) {
 79:   $sql_str .= sprintf("order by serialid desc limit %d", $range);
 80: } else {
 81:   $sql_str .= sprintf(" and top = %d order by serialid ", $thread);
 82:   if (isset($offset)) {
 83:     $sql_str .= sprintf("limit %d, %d", $offset, $range);
 84:   } else {
 85:     $sql_str .= sprintf("limit 0, %d", $range);
 86:   }
 87: }
 88: $result = mysql_query($sql_str) or die('SQLエラー'.$sql_str);
 89: ?>
 90: <TITLE><?=$board_name?> 一覧表示</TITLE>
 91: </head>
 92: <body>
 93: <div class="bbstitle">
 94:   <?=$board_name?>
 95: </div>
 96: <br>
 97: <?=$board_message?>
 98: <br>
 99:   [<a href="./list.php?boardid=<?=$board_id?>">日付順表示</a>]
100:   [<a href="./list.php?boardid=<?=$board_id?>&amp;mode=index">日付順インデックス</a>]
101:   [<a href="./thread.php?boardid=<?=$board_id?>">スレッド順インデックス</a>]<br>
102:   <br>
103:  <hr>
104:  <center>
105:  <a href="./form.php?boardid=<?=$board_id?>">新規投稿</a> |
106:  <a href="<?=$homepage?>">開設者ホームページへ戻る</a> |
107:  <a href="http://kmaebashi.com/bbshelp.html">ヘルプ</a>
108:  </center>
109:  <hr>
110: 
111: <?php
112: if ($mode == 'index') {
113:   echo "<table>";
114: }
115: ?>
116: <?php
117: while ($row = mysql_fetch_assoc($result)) {
118:   $serialid=$row["serialid"];
119:   $admindeleted = $row["admindeleted"];
120:   $subject = htmlspecialchars($row["subject"]);
121:   $name = htmlspecialchars($row["name"]);
122:   $url = htmlspecialchars($row["url"]);
123:   $message = htmlspecialchars($row["message"]);
124:   $altermessage = htmlspecialchars($row["altermessage"]);
125:   $top = $row["top"];
126:   $date = format_date($row["posteddate"]);
127:   $id = create_id($row["ipaddress"]);
128: 
129:   if (!isset($firstid)) {
130:     $firstid = $serialid;
131:   }
132:   $lastid = $serialid;
133: 
134:   if ($row["deleted"]) {
135:     continue;
136:   }
137:   if ($row["admindeleted"] && $mode == "plain") {
138:     include 'admindeletedplain.php';
139:   } else if ($row["admindeleted"] && $mode == "index") {
140:     include 'admindeletedindex.php';
141:   } else if ($mode == 'plain') {
142:     include 'plain.php';
143:   } else if ($mode == 'index') {
144:     include 'index.php';
145:   }
146:   if ($mode == 'plain') {
147: ?>
148: <div align="center">
149: [<a href="./thread.php?boardid=<?=$board_id?>&amp;from=<?=$top?>&amp;range=1">
150: この投稿を含むスレッドを表示</a>]
151: [<a href="./delete.php?boardid=<?=$board_id?>&amp;serialid=<?=$serialid?>">
152: この投稿を削除</a>]
153: </div>
154: <br><br>
155: <?php
156:   }
157: }
158: if ($mode == 'index') {
159:   echo "</table>";
160: }
161: if (!isset($firstid)) {
162:   die("該当するレスなし。");
163: }
164: ?>
165: <hr>
166: <div align="center">
167: <?php
168: $sql_str = sprintf("select min(serialid), max(serialid) "
169:                    . "from message where boardid='%s' ",
170:                    $board_id);
171: if (isset($thread)) {
172:   $sql_str .= sprintf("and top = %d", $thread);
173: }
174: $result = mysql_query_or_die($sql_str);
175: $row = mysql_fetch_row($result);
176: $db_min = $row[0];
177: $db_max = $row[1];
178: 
179: if (!isset($thread)) {
180:   # 通常表示
181:   if ($db_max > $firstid) {
182:     $prevlink = sprintf("./list.php?boardid=%s&amp;from=%d&amp;range=%d&amp;mode=%s",
183:                         $board_id, $firstid + $range, $range, $mode);
184:     $prevmessage = "より新しい投稿";
185:   }
186: } else {
187:   # スレッド一覧
188:   if (isset($offset) && $offset > 0) {
189:     $prevlink = sprintf("./list.php?boardid=%s&amp;range=%d&amp;mode=%s"
190:                         . "&amp;thread=%d",
191:                         $board_id, $range, $mode, $thread);
192:     $new_offset = $offset - $range;
193:     if ($new_offset > 0) {
194:       $prevlink .= sprintf("&amp;offset=%d", $new_offset);
195:     }
196:     $prevmessage = "より古い投稿";
197:   }
198: }
199: if (isset($prevlink)) {
200: ?>
201: [<a href="<?=$prevlink?>">
202: <?=$prevmessage?></a>]
203: <?php
204: }
205: if (!isset($thread)) {
206:   # 通常表示
207:   if ($db_min < $lastid) {
208:     $nextlink = sprintf("./list.php?boardid=%s&amp;from=%d&amp;range=%d&amp;mode=%s",
209:                         $board_id, $lastid - 1, $range, $mode);
210:     $nextmessage = "より古い投稿";
211:   }
212: } else {
213:   # スレッド一覧
214:   if ($db_max > $lastid) {
215:     if (isset($offset)) {
216:       $new_offset = $offset + $range;
217:     } else {
218:       $new_offset = $range;
219:     }
220:     $nextlink = sprintf("./list.php?boardid=%s&amp;range=%d&amp;mode=%s"
221:                         . "&amp;thread=%d&amp;offset=%d",
222:                         $board_id, $range, $mode, $thread, $new_offset);
223:     $nextmessage = "より新しい投稿";
224:   }
225: }
226: if (isset($nextlink)) {
227: ?>
228: [<a href="<?=$nextlink?>">
229: <?=$nextmessage?></a>]
230: <?php
231: }
232: ?>
233: </div>
234: </body>
235: </html>

削除関係以外にも、色々いじっています。

14行目で、get_css($board_id)という関数を呼んでCSSの設定をしていますが、 この関数はutil.phpで定義されていて、 boardテーブル中のcssフィールドを返します。 これにより、板ごとに異なるCSSが適用できるわけで、 浮かれた私は2004年のクリスマスやら2005年の正月やらに CSSを変更してみたのですが反応はまったく無かったのでした。 (´・ω・`)ショボーン。

97行目では、板ごとのメッセージを表示しています。 これは、boardテーブルのmessageフィールドに指定されたHTMLです。

また、

さて、この掲示板では、投稿者削除を行っても管理者削除を行っても、 発言そのものは「なかったこと」にはなりません。 D/B中には最初の発言が必ず残りますし、発言番号もそのままです。

となると、list.phpは、boardテーブルの設定に従い 「日付順表示のときは何件ずつ表示」、 「インデックス表示のときは何件ずつ表示」という制御を行っているはずですが、 この時、削除された投稿はどう扱うべきでしょうか。

一番簡単なのは、削除された投稿もそこにあるものと数え、 表示だけを抑制する方法です。 そして、ここではその一番簡単な方法を選びました。 134〜136行目のcontinueがそれです。 そのため、投稿者削除があると、見かけ上1ページあたりの表示数が減ります。

管理者削除の際は代替メッセージを表示します。 これはadmindeletedplain.phpをincludeすることで表示しています(138行目)。 そのソースはこちら。

  1: <DIV class="altermessage">
  2: <DIV style="margin: 10px 10px 10px 10px;">
  3: <BR>
  4: <DIV style="line-height:0%;">
  5:   [<?=$serialid?>]
  6:   <STRONG><font size="4">管理者により削除されました</font></STRONG>
  7:   <DIV align="right">
  8:   <a href="./form.php?boardid=<?=$board_id?>&parent=<?=$serialid?>">返信</a></DIV>
  9:   </DIV>
 10:    <BR>
 11:    <DIV align="right"><?=$date?></DIV>
 12:    <HR>
 13: <TT>
 14: <?php
 15: $message2 = convert_message($altermessage);
 16: ?>
 17: <?=$message2?>  
 18: </TT>
 19: </DIV>
 20: </DIV>

なお、投稿者削除を行うと、 (発言番号は飛んでしまうものの)見かけ上その投稿は消えてなくなるわけですが、 スレッド表示の場合はそういうわけにもいきません。 その投稿に返信されていると、ツリーが崩れるからです。

そこで、threaditem.phpにて、「投稿者により削除されました」または 「管理者により削除されました」という文字列を表示することにしました。

  1: <?php
  2: $serialid=$row["serialid"];
  3: $subject = htmlspecialchars($row["subject"]);
  4: $name = htmlspecialchars($row["name"]);
  5: $date = format_date($row["posteddate"]);
  6: ?>
  7: [<?=$serialid?>]
  8: <?php
  9: if ($row["deleted"]) {
 10:   echo "投稿者により削除されました";
 11: } else {
 12: ?>
 13: <a href="./list.php?boardid=<?=$board_id?>&from=<?=$serialid?>&range=1">
 14: <?php
 15:   if ($row["admindeleted"]) {
 16:     echo "管理者により削除されました";
 17:   } else {
 18:     echo $subject;
 19:   }
 20: ?>
 21: </a>
 22: <?=$name?> 
 23: <?=$date?>
 24: <?php
 25: }
 26: ?>

ところで、削除機能とは関係ないですが、現状の掲示板では、 plain.phpにて、投稿者のIPアドレスをMD5変換したものをコメント中に埋め込んでいます (もちろんこれにも秘密の文字列は付けてあります)。

  1: <DIV class="message">
  2: <DIV style="margin: 10px 10px 10px 10px;">
  3: <BR>
  4: <!--REMOTEHOST_ID..<?=$id?>-->
  5: <DIV style="line-height:0%;">
  6:   [<?=$serialid?>]
  7:   <STRONG><font size="4">
  8:     <?= $subject ?>
  9:   </font></STRONG>
 10:   <DIV align="right">
 11:   <a href="./form.php?boardid=<?=$board_id?>&parent=<?=$serialid?>">返信</a></DIV>
 12:   </DIV>
 13:    <BR>
 14:    <BR>
 15:    <DIV style="line-height:0%;">
 16:      投稿者:<?= $name ?>
 17:     <DIV align="right"><?=$date?></DIV>
 18:    </DIV>
 19:    <BR>
 20:    Link:<a href="<?=$url?>"><?=$url?></a>
 21:    <HR>
 22: <TT>
 23: <?php
 24: $message2 = convert_message($message);
 25: ?>
 26: <?=$message2?>  
 27: </TT>
 28: </DIV>
 29: </DIV>

4行目がそれです。

自作自演防止のための機能です。 現状では、うちの掲示板では不要な機能だと思いますが。

締め?

「この文章については、一気にガシガシ書いていこうと思います」 と書いておきながら、えらく長引いてしまいましたが、 「PHPとMySQLで掲示板を作る」は、ひとまずこれで「締め」にしたいと思います。

全体のソースのダウンロードは、要望があればやるかもしれませんが、 ここでやりたかったのは「掲示板の作り方」を説明することだったわけで、 積極的にやる気はないです。

今後、何らかの機能追加を行ったら、ここで報告するかもしれません。 それまでは、ひとまず完結、ということにさせていただきますです。

ご愛読ありがとうございました。



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

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