【SQLite+PHP】掲示板の実装2

スレッド付きBBS PHP
スレッド機能付きBBS

スレッド機能を実装した掲示板です。スレッドの追加と投稿は誰でもできますが、スレッドと投稿の削除は管理者のパスワードがないとできない仕様です。削除した投稿やスレッドは、DB上に残っているのでSQLを使って復元できるようになっています。

環境

 OS ubuntu22.04 
 Webサーバ apache2.4.52(自身のIP 192.168.19.128、公開フォルダ /var/www/html)
 SQLite 3.37.2
 PHP 8.1.2-1ubuntu2.14

DBの作成

掲示板のデータを格納するDBを作成するSQLは次の通りです。

$ > sqlite3 bbs2.db
sqlite > CREATE TABLE threads (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, created_by TEXT, is_deleted INTEGER);
sqlite > CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, content TEXT, created_by TEXT, is_deleted INTEGER, posts_id integer);
sqlite > CREATE TABLE admin (password TEXT);

threads スレッドデータの格納用 is_deleted が削除フラグ
posts が掲示板データの格納用 is_deleted が削除フラグ
admin が削除用管理者パスワード保存用
 パスワードはSHA-256によるハッシュ値で保存しているので、万が一データベースを見られてもパスワードはわかりません。
 出力されたbbs2.dbファイルは、次のフォルダに置いてパーミッションを707に変更しました。

/var/www/db/

PHPプログラムの作成(管理者パスワード設定画面)

今回は、管理者用の画面から作成します。
次のPHPプログラムを配置しました。

ファイル名 admin.php
<?php
$db = new SQLite3('/var/www/db/bbs2.db');

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['new_password'])) {
    $new_password = $_POST['new_password'];
    // 新しいパスワードをハッシュ化して保存する
    $hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
    $db->exec("INSERT OR REPLACE INTO admin (password) VALUES ('$hashed_password')");
}
// 現在の管理者パスワードを取得する
$current_password = $db->querySingle("SELECT password FROM admin");

// HTMLを出力する
?>
<!DOCTYPE html>
<html>
<head><title>管理者用パスワードの設定</title></head>
<body>
    <h1>管理者用パスワードの設定</h1>
    <form method="post" action="admin.php">
        <label for="new_password">新しいパスワード:</label>
        <input type="password" id="new_password" name="new_password" required />
        <input type="submit" value="パスワードを変更" />
    </form>
    <p>現在のパスワード: <?php echo $current_password ? $current_password : "未設定"; ?></p>
</body>
</html>

このプログラムは、管理者用のパスワードを設定するためのページです。
前のパスワードを知らなくても新しいパスワードを設定できるので、アクセス制限がかかっているページに置かないと危険です。
 次の場所に配置し、パーミッションは705に設定しました。
  /var/www/html/php

PHPプログラムの作成(スレッド表示画面)

続いてスレッド表示用の画面を作成します。
次のPHPプログラムを配置しました。

ファイル名 thread.php
<?php
$db = new SQLite3('/var/www/db/bbs2.db');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['thread_title'])) {
        $thread_title = $_POST['thread_title'];

        // 新しいスレッドを追加する
        $stmt = $db->prepare('INSERT INTO threads (title) VALUES (:title)');
        $stmt->bindValue(':title', $thread_title, SQLITE3_TEXT);
        $stmt->execute();
        
    } elseif (isset($_POST['delete_thread_id']) && isset($_POST['admin_password'])) {
        $admin_password = $_POST['admin_password'];
        $delete_thread_id = $_POST['delete_thread_id'];

        // 管理者パスワードが正しいか確認する
        $admin_check = $db->querySingle("SELECT password FROM admin");
        if (password_verify($admin_password, $admin_check)) {
            // スレッドを削除する
            $db->exec("update threads set is_deleted = 1 WHERE id = $delete_thread_id");
            $db->exec("update posts set is_deleted = 1 WHERE thread_id = $delete_thread_id");
        }
    }
}

// スレッド一覧を取得する
$threads = $db->query('SELECT * FROM threads where is_deleted is null');
// HTMLを出力する
?>
<!DOCTYPE html>
<html>
<head><title>掲示板</title></head>
<body>
    <h1>スレッド一覧</h1>
    <table border=1 width="700px"><tr><td>
        <?php while ($thread = $threads->fetchArray()) : ?>
            <a href="posts.php?thread_id=<?= $thread['id'] ?>"> <?= $thread['id'] ?>: <?= $thread['title'] ?></a>
        <?php endwhile; ?>
        </td></tr></table>
    </ul>
    <h2>新しいスレッドを追加</h2>
    <form method="post" action="thread.php">
        <input type="text" name="thread_title" placeholder="スレッドのタイトル" required  style="width:400px;"/>
        <input type="submit" value="スレッドを追加" />
    </form>
    <h2>スレッドの削除</h2>(スレッドの削除には管理者パスワードが必要です)<br>
    <form method="post" action="thread.php">
      削除したいスレッドID<input type="text" name="delete_thread_id" value="" style="width:30px;" />
      管理者用パスワード<input type="password" name="admin_password" placeholder="削除用パスワード" />
      <input type="submit" value="スレッド削除" />
    </form>
</body>
</html>

このページはスレッドの一覧表示と登録画面です。管理者パスワードがあればスレッドを削除することができるようになっています。
 スレッドを削除すると、スレッドと紐づいている投稿の is_deleted に1を入れる仕様になっていますので、削除したスレッドの復元はSQLで簡単に行えます。
 参考までにスレッド番号「14」を復元するSQLを書いておきます。

$ > sqlite3 bbs2.db
sqlite > update threads set is_deleted = 0 WHERE id =14;
sqlite > update posts set is_deleted = 0 WHERE thread_id = 14;

PHPプログラムの作成(投稿表示画面)

続いて投稿表示用の画面を作成します。
次のPHPプログラムを配置しました。

ファイル名 posts.php
<?php
$db = new SQLite3('/var/www/db/bbs2.db');


if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['content']) && isset($_POST['thread_id'])) {
    $content = $_POST['content'];
    $thread_id = $_POST['thread_id'];
    $created_by = $_POST['created_by'];
    //投稿番号を生成
    $stmt = $db->prepare('SELECT (COALESCE(MAX(posts_id), 0) + 1)as maxcount from posts where thread_id = :thread_id');
    $stmt->bindValue(':thread_id', $thread_id, SQLITE3_INTEGER);
    $post = $stmt->execute();
    $max_count = 1;
    if ($row = $post->fetchArray()) {
      $max_count = $row['maxcount'];
    }
    
    $stmt = $db->prepare('INSERT INTO posts (thread_id, content, created_by, is_deleted,posts_id) VALUES (:thread_id, :content, :created_by, 0, :posts_id)');
    $stmt->bindValue(':thread_id', $thread_id, SQLITE3_INTEGER);
    $stmt->bindValue(':content', $content, SQLITE3_TEXT);
    $stmt->bindValue(':created_by', $created_by, SQLITE3_TEXT);
    $stmt->bindValue(':posts_id', $max_count, SQLITE3_TEXT);
    $stmt->execute();
} elseif (isset($_POST['delete_posts_id']) && isset($_POST['admin_password']) && isset($_POST['thread_id'])) {
        $admin_password = $_POST['admin_password'];
        $delete_posts_id = $_POST['delete_posts_id'];
        $thread_id = $_POST['thread_id'];
        
        // 管理者パスワードが正しいか確認する
        $admin_check = $db->querySingle("SELECT password FROM admin");
        if (password_verify($admin_password, $admin_check)) {
            // スレッドを削除する
            $db->exec("UPDATE posts set is_deleted = 1 WHERE thread_id = $thread_id and posts_id = $delete_posts_id");
        }
    } 

//スレッド番号はPOST,GETどちらでも取得可能
if (isset($_GET['thread_id'])) {
    $thread_id = $_GET['thread_id'];
} elseif(isset($_POST['thread_id'])) {
    $thread_id = $_POST['thread_id'];
}

if (isset($thread_id)) {

    // 対応するスレッドの投稿を取得する
    $stmt = $db->prepare('SELECT * FROM posts WHERE thread_id = :thread_id AND is_deleted = 0');
    $stmt->bindValue(':thread_id', $thread_id, SQLITE3_INTEGER);
    $posts = $stmt->execute();
    $thread_title = $db->querySingle("SELECT title FROM threads WHERE id = $thread_id");
}

if(isset($thread_title)){
    // 投稿後に投稿記事一覧を表示
    echo "<table border=1 width='700px'><tr><td>";
    echo "<h1>$thread_id: $thread_title</h1>";
    
    while ($post = $posts->fetchArray()) {
        echo $post['posts_id']." 名前: " . $post['created_by']."<br>" ;
        echo "<input type='hidden' name='thread_id' value='$thread_id' />";
        echo "<dd>".$post['content']."</dd><br>";

    }
    echo "</td></tr></table>";
    
    // 投稿フォームを表示
    echo "<h2>新しい投稿を追加</h2>";
    echo "<form method='post' action='posts.php'>";
    echo "  <input type='hidden' name='thread_id' value='$thread_id' width='40px'/>";
    echo "  <input type='text' name='created_by' placeholder='投稿者名' required /><br><br>";
    echo "  <textarea name='content' placeholder='投稿内容' required></textarea><br><br>";
    echo "  <input type='submit' value='投稿' />";
    echo "</form>";
    echo "<h2>スレッドの削除</h2>(スレッドの削除には管理者パスワードが必要です)<br>";
    echo "<form method='post' action='posts.php'>";
    echo "  削除したい投稿ID<input type='text' name='delete_posts_id' value='' style='width:30px;' />";
    echo "  <input type='hidden' name='thread_id' value='$thread_id' />";
    echo "  管理者用パスワード<input type='password' name='admin_password' placeholder='削除用パスワード' />";
    echo "  <input type='submit' value='スレッド削除' />";
    echo "</form>";

}else {
    echo "<h2>スレッドが見つかりません。</h2>";
}
echo "<a href='./thread.php'>スレッド一覧に戻る</a>";
?>

 このPHPはスレッドに紐づいている投稿の一覧表示と投稿画面です。管理者パスワードがあれば投稿を削除することができるようになっています。
 削除した投稿の is_deleted に1を入れる仕様になっていますので、復元するには次のSQLで簡単に行えます。
 参考までにスレッド番号「thread_id =7」の投稿番号「posts_id =11」を復元するSQLを書いておきます。

$ > sqlite3 bbs2.db
sqlite > update posts set is_deleted = 0 WHERE thread_id = 7 and posts_id = 11;

posts_idだけを指定すると、すべてのスレッド上の同じposts_idを指定することになるので、必ずthread_idとposts_idを指定するようにしてください。
 作成したら、thread.php、posts.php、admin.php(ほかの場所に配置してもOK)を
   /var/www/html/php
に配置して、パーミッションを705に変更します。

実行

 まずは管理者ページでブラウザから管理者パスワードを設定します。
 http://192.168.19.128/php/admin.php

admin.phpを実行した様子

パスワードのハッシュ値が表示されています。
 続いて、http://192.168.19.128/php/thread.phpを開くとこんな感じになります。

thread.phpを実行した様子

スレッド名からリンク先を開くと、posts.phpが開きます。

posts.phpを実行した様子

ここでは記事の投稿と閲覧ができます。
 CSSを使ったりHTMLを工夫すれば見た目もまともになると思います。