17
@torokko

PHP+SQLite3で2ちゃんねるっぽい掲示板を作る

この記事は最終更新日から5年以上が経過しています。

環境

CentOS 6.5
Apache 2.2.15
php 5.5.19
sqlite3 3.6.20

構成

  • dbファイルはdbフォルダを作ってそこに入れておく
    -- 各掲示板情報を管理するdb(list.db)
    -- ちなみにパーミッション注意。dbフォルダは777,dbファイルはたしか755or766)

  • phpファイルは以下のとおり。
    -- 掲示板リストを表示する(list.php)
    -- 各掲示板(bbs.php)
    -- アンカーをクリックしたときの処理(link.php)
    -- 新規投稿処理、結果(ins.php)
    -- 掲示板追加処理(build.php)
    -- 掲示板削除処理、結果(del.php)

予め準備するDBの内容

list.db
CREATE TABLE list (
id INTEGER PRIMARY KEY, 
dbname TEXT, 
dbnamekana TEXT, 
kensu INTEGER, 
updtime TEXT DEFAULT (DATETIME('now', '+09:00:00')), 
flg INTEGER DEFAULT 0
);

// 具体的な各掲示板のDBは動的に生成するので予め用意する必要はない。
// dbnameは掲示板生成時に自動的にbbs1,bbs2・・・と名付けされる
// kensuは各掲示板に投稿されている投稿件数のこと
// updtimeは掲示板更新日時で、リスト表示するときにこの順番で並べる
// flg(フラグ)は将来的に過去スレ行きを識別するために使うとかです。

具体的なコード

list.php
<?php
$db = new PDO("sqlite:db/list.db");

// 掲示板の数を取得しておく
$stmt = $db->query("SELECT COUNT(*) FROM list");
$listcount = $stmt->fetchColumn();
$stmt->closeCursor();

$i = 1;
$html = "";

// 掲示板が1つ以上存在すればリストをリンク付きで表示する
if ($listcount > 0) {
  // 新規投稿があった順に並べる
  $stmt = $db->query("SELECT * FROM list WHERE flg=0 ORDER BY updtime DESC");
  $list = $stmt->fetchAll(PDO::FETCH_ASSOC);
  foreach ($list as $row) {
    $html .= "<a href='bbs.php?dbname={$row["dbname"]}&dbnamekana={$row["dbnamekana"]}'>".$i.":".$row["dbnamekana"]." (".$row['kensu'].") ".$row['updtime']."</a>"." <a href='del.php?dbname={$row["dbname"]}'>削除する</a><br><br>";
  $i++;
  }
}
$db = null;

// 掲示板の数が10未満であれば新スレ作成フォームを表示する。
if ($listcount < 10) {
  $listcount++;
  $newdbname = "bbs".$listcount;
  $build = <<< EOM
  <form action="build.php" method="post">
  <input type="hidden" name="newdbname" value="{$newdbname}">
  名称<input type="text" name="newdbnamekana" value="">
  <input type="submit" name="newbbs" value="新しく掲示板を立てる(制限10個)">
  </form>
EOM;
} else {
  $build = "これ以上掲示板は立てられません。";
}

?>
<!DOCTYPE html>
<html lang="ja">
<head>
<title>サンプル掲示板</title>
<meata charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0">
<link rel="stylesheet" href="style.css" media="all">
</head>
<body>
<h2 style="color:#ff0000;">サンプル掲示板</h1>
<?php echo $html; ?>
<?php echo $build; ?>
</body>
</html>
bbs.php
<?php
if (isset($_GET["dbname"]) && isset($_GET["dbnamekana"])) {
  $dbname = htmlspecialchars($_GET["dbname"], ENT_QUOTES);
  $dbnamekana = htmlspecialchars($_GET["dbnamekana"], ENT_QUOTES);
  if (isset($_GET["page"])) {
    $page = htmlspecialchars($_GET["page"], ENT_QUOTES);
  } else {
    $page = "";
  }
} else {
  header("Location: list.php");
}

// ページングするための情報取得、設定
$name = "sqlite:db/".$dbname.".db";
$db = new PDO($name);
$stmt = $db->query("SELECT COUNT(*) FROM bbs");
$kensu = $stmt->fetchColumn(); // 総件数
$hyouji = 3; // 1ページあたり表示件数
$pcount = ceil($kensu/$hyouji); // 総ページ数
$plast = $kensu % $hyouji; // 最終ページの件数
if ($page == "") {
  $page = $pcount;
}

// ページ移動リンク
$link = "";
for ($i=1; $i<=$pcount; $i++) {
  $link .= "<a href=bbs.php?dbname=".$dbname."&dbnamekana=".$dbnamekana."&page=".$i.">".$i."</a>&nbsp;&nbsp;";
}

// ページ表示
$stmt = $db->query("SELECT * FROM bbs LIMIT ($page-1)*$hyouji, $hyouji");
$html = "<h2 style='color:#ff0000;'>".$dbnamekana."</h2>";
$list = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($list as $row) {
  $html .= $row["id"]." : ".$row["hiduke"]."<br>";
  $html .= "<div id='commnet'>".$row["comment"]."</div><br><br>";
}

// コメントにアンカーがあればアンカーに変換する
$html = anchor($dbname, $html);
$db = null;

// 総件数が10件未満であれば投稿画面を表示する
if ($kensu < 10) {
  $form = <<< EOM
  <form action="ins.php" method="post">
  <input type="hidden" name="dbname" value="{$dbname}">
  投稿内容:<br>
  <textarea name="comment" cols=100 rows=10></textarea><br>
  <input type="submit" name="ins" value="送信">
  </form>
  <br>
EOM;
} else {
  $form = "10件以上は書き込めません。<br><br>";
}

// コメント中にアンカーがあればアンカーリンクを付与する
function anchor($dbname, $comment) {
  $pattern = "/&gt;&gt;([0-9])+/";
  $replace = '<a href="link.php?dbname='.$dbname.'&num=$0">$0</a>';
  $comment = preg_replace($pattern, $replace, $comment);
  return $comment;
}

?>

<!DOCTYPE html>
<html lang="ja">
<head>
<title><?php echo $dbnamekana; ?></title>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0">
<link rel="stylesheet" href="style.css" media="all">
</haed>
<body>
<?php echo $html; ?>
<?php echo $form; ?>
<?php echo $link; ?>
<br><br>
<a href="list.php">掲示板に戻る</a>
</body>
</html>
ins.php
<?php
// 投稿日時等を取得する
$w = date("w");
$week_name = ["日", "月", "火", "水", "木", "金", "土"];
$hiduke = date("Y-m-d H:i:s")."(".$week_name[$w].")";

$html = "";

// データ受取、変換処理
if (isset($_POST["ins"])) {
  $dbname = htmlspecialchars($_POST["dbname"], ENT_QUOTES);
  $comment = nl2br(htmlspecialchars($_POST["comment"], ENT_QUOTES));
  $comment = anchor($comment);

 // 軽く制限かけてみた
  if (check_br($comment) > 10) {
    $html .= "改行が多過ぎます。10行以内でお願いします。<br><br>";
  } else if (check_strcount($comment) > 100) {
    $html .= "文字数が多過ぎます。100文字以内でお願いします。<br><br>";
  } else {
  
  // 投稿処理
    $name = "sqlite:db/".$dbname.".db";
    $db = new PDO($name);
    $db->exec("INSERT INTO bbs (hiduke, comment) VALUES ('$hiduke', '$comment')");

  // 件数を取得し直す
    $stmt = $db->query("SELECT COUNT(*) FROM bbs");
    $kensu = $stmt->fetchColumn();

  // list.dbの件数、更新日時を更新しておく(これで掲示板リスト画面で一番上に表示されるようになる)
  $db = new PDO("sqlite:db/list.db");
  $db->exec("UPDATE list SET kensu=$kensu, updtime=(SELECT datetime('now', '+09:00:00')) WHERE dbn
ame='$dbname'");
    $db = null;
    $html .= "投稿しました。<br><br>";
  }
} else {
  header("Location: list.php");
}

// アンカーをリンク付きに変換する
function anchor($comment) {
  $pattern = "/>>([0-9])+/";
  $replace = '<a href="link.php?num=$0">$0</a>';
  $comment = preg_replace($pattern, $replace, $comment);
  return $comment;
}

// 改行数をカウントする
function check_br($comment) {
  $brcount = mb_substr_count($comment, "<br />");
  return $brcount;
}

// 文字数をカウントする
function check_strcount($comment) {
  $strcount = strlen($comment);
  return $strcount;
}

?>
<!DOCTYPE html>
<html lang="ja">
<head>
<title>サンプル掲示板</title>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0">
<link rel="stylesheet" href="style.css" media="all">
</head>
<body>
<?php echo $html; ?>
<a href=list.php>掲示板に戻る</a>
</body>
</html>
link.php
// アンカーをクリックしたときにそのコメントを別ページに表示
<?php
if (isset($_GET["dbname"]) && isset($_GET["num"])) {
  $dbname = htmlspecialchars($_GET["dbname"]);
  $num = htmlspecialchars($_GET["num"]);
  $num = substr($num, 8);
  $name = "sqlite:db/".$dbname.".db";
  $db = new PDO($name);
  $stmt = $db->query("SELECT * FROM bbs WHERE id=$num");
  $html = "";
  while ($row = $stmt->fetch()) {
    $html .= $row["id"].":".$row["hiduke"]."<br>".$row["comment"];
  }
  $db = null;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<title>サンプル掲示板</title>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0">
<link rel="stylesheet" href="style.css" media="all">
</head>
<body>
<?php echo $html; ?>
</body>
</html>
build.php
// 掲示板の追加処理
<?php
if (isset($_POST["newbbs"])) {
  $newdbname = htmlspecialchars($_POST["newdbname"], ENT_QUOTES);
  $newdbnamekana = htmlspecialchars($_POST["newdbnamekana"], ENT_QUOTES);
  $db = new PDO("sqlite:db/list.db");
  $db->exec("INSERT INTO list (dbname, dbnamekana) VALUES ('$newdbname', '$newdbnamekana')");
  $name = "db/".$newdbname.".db";
  $db = new SQLite3($name);
  $db->exec("CREATE TABLE bbs (id INTEGER PRIMARY KEY, hiduke TEXT, comment TEXT)");
  $db = null;
  echo "新しい掲示板「".$newdbnamekana."」を作りました。<br><br>";
  echo "<a href='list.php'>掲示板へ戻る</a>";
}
?>
del.php
// 掲示板の削除処理
<?php
if (isset($_GET["dbname"])) {
  $dbname = htmlspecialchars($_GET["dbname"], ENT_QUOTES);
  $db = new PDO("sqlite:db/list.db");
  $db->exec("DELETE FROM list WHERE dbname='$dbname'");
  $name = "db/".$dbname.".db";
  // ファイルごとにスレがあるのでファイルごと消せばいい
  unlink($name);
  echo "削除しました。<br><br>";
  echo "<a href='list.php'>掲示板へ戻る</a>";
}
?>
style.css
body {
  // background-color: #D3D3D3;
  background-color: #F5F5F5;
}

#comment {
  margin-left: 35px;
}

最後に

いろいろと説明が下手くそで申し訳ありません。より本格的に作るためにはクッキーやセッション、正規表現などを駆使してある程度の荒らし対策が必要になってくるかと思います。

17
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
torokko
基本的に備忘録代わりのつもりですが、親切な方から有益なコメントを頂けるのをちょっぴり期待しつつ、いろいろ書いていこうと思っています。
この記事は以下の記事からリンクされています

コメント

SQLインジェクションができるのでとりあえず公開を停止しましょう

0

ありがとうございます。停止しました。

0
(編集済み)

パッとみわかる問題点として
SQL文に文字列を連結してしまっている

$db->exec("UPDATE list SET kensu=$kensu, updtime=(SELECT datetime('now', '+09:00:00')) WHERE dbn
ame='$dbname'");

これは

$stmt = $db-> prepare("UPDATE list SET kensu=?, updtime=(SELECT datetime('now', '+09:00:00')) WHERE dbn
ame='?'");
$stmt->execute([$kensu,$dbname]);

こうする

0

また、list.dbファイルにそのままアクセスできてしまうので
htaccessでアクセスを制限したり公開でないディレクトリに移動するといいと思います

0

あと
htmlspecialcharsでエスケープするのは
出力時の方がいいです
http://blog.tokumaru.org/2012/04/bad-sanitization-good-sanitization-and.html

0

ファイルにそのままアクセスってことはwgetとかでダウンロードできちゃうとかですかね。sqlの渡し方はいろいろやり方があって自分のなかでよく確立されていなかったのですがインジェクション対策と合わせて深く検討したいと思います。最初は正直かなりへこみましたが、アドバイスたくさんありがとうございました。

0

安全なウェブサイトの作り方を読まれると良いかなって気がします。。。

0

ありがとうございます。まずはそれから読んでみたいと思います。

0

@torokko
list.dbはwgetとか関係なくブラウザから直で叩くとダウンロードできてしまいます。

0

3upjpさん、ありがとうございます。とりあえず徳丸氏の本を購入しました。これから勉強します。

0
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
フロントエンド強化月間 - 開発する上で知っておくべき知見を共有しよう
~
Azure AIを活用した機械学習に関する記事を投稿しよう!
~