首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP BBS 系統完整版(一):發文表單與資料驗證重構

     編輯:PHP 綜合知識

前言

原始 say.php 混雜:身分驗證、CSRF 缺失、SQL 拼接、HTML/JS 混寫、硬編碼圖片路徑、htmlspecialchars 僅用於輸出。本文重構為:

  • Controller 負責流程、驗證、資料庫操作
  • Service 封裝業務規則
  • Twig 模板專注呈現
  • 統一錯誤處理、CSRF、輸出編碼

資料表設計

CREATE TABLE `bbs_posts` (
    `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    `parent_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '回覆對象,NULL=主題',
    `user_id` VARCHAR(64) NOT NULL COMMENT '發文者 ID',
    `title` VARCHAR(200) NOT NULL,
    `content` MEDIUMTEXT NOT NULL,
    `ip` VARBINARY(16) NOT NULL,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    INDEX `idx_parent` (`parent_id`),
    INDEX `idx_user_time` (`user_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Controller(say.php)

<?php
// say.php
declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php'; // $pdo, $twig, $session, $csrf

$replyTo = (int)($_GET['reply_to'] ?? 0);
$parent = null;
if ($replyTo > 0) {
    $stmt = $pdo->prepare('SELECT id, title FROM bbs_posts WHERE id = ?');
    $stmt->execute([$replyTo]);
    $parent = $stmt->fetch(PDO::FETCH_ASSOC);
}

// 必須登入
$userId = $session->get('user_id');
if (!$userId) {
    $_SESSION['flash'] = ['type' => 'warning', 'msg' => '請先登入'];
    header('Location: login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
    exit;
}

// 處理 POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
        $errors['_form'] = '表單驗證失敗,請重新整理';
    } else {
        $title = trim($_POST['title'] ?? '');
        $content = trim($_POST['content'] ?? '');
        if ($title === '') $errors['title'] = '標題不可為空';
        if (mb_strlen($title) > 200) $errors['title'] = '標題過長';
        if ($content === '') $errors['content'] = '內容不可為空';
        if (mb_strlen($content) > 1_000_000) $errors['content'] = '內容過長';

        if (empty($errors)) {
            try {
                $pdo->beginTransaction();
                $stmt = $pdo->prepare('INSERT INTO bbs_posts (parent_id, user_id, title, content, ip) VALUES (?, ?, ?, ?, ?)');
                $stmt->execute([
                    $replyTo ?: null,
                    $userId,
                    $title,
                    $content,
                    inet_pton($_SERVER['REMOTE_ADDR'])
                ]);
                $newId = $pdo->lastInsertId();

                // 若是回覆,更新父主題的回覆計數或最後回覆時間(可擴充)
                if ($replyTo) {
                    $pdo->prepare('UPDATE bbs_posts SET updated_at = NOW() WHERE id = ?')->execute([$replyTo]);
                }

                $pdo->commit();
                $_SESSION['flash'] = ['type' => 'success', 'msg' => '發文成功'];
                header('Location: thread.php?id=' . ($replyTo ?: $newId));
                exit;
            } catch (Throwable $e) {
                $pdo->rollBack();
                $errors['_form'] = '資料庫錯誤:' . $e->getMessage();
            }
        }
    }
}

// 顯示表單
echo $twig->render('say.twig', [
    'parent' => $parent,
    'replyTo' => $replyTo,
    'csrf' => generateCsrfToken(),
    'errors' => $errors ?? [],
    'old' => ['title' => $_POST['title'] ?? ($parent ? 'Re: ' . $parent['title'] : ''), 'content' => $_POST['content'] ?? ''],
    'flash' => $_SESSION['flash'] ?? null
]);
unset($_SESSION['flash']);
?>

Twig 模板(say.twig)

{# templates/say.twig #}



    
    {{ parent ? '回覆:' ~ parent.title|e : '發表新主題' }}
    
    


{{ parent ? '回覆主題' : '發表新主題' }}

返回列表
{% if flash %}
{{ flash.msg }}
{% endif %} {% if parent %}
原主題
{{ parent.title|e }}
{% endif %}
{% if errors.title is defined %}
{{ errors.title }}
{% endif %}
{% if errors.content is defined %}
{{ errors.content }}
{% endif %}
支援 Markdown,最多 1MB
{% if errors._form is defined %}
{{ errors._form }}
{% endif %}
取消

關鍵安全與品質強化

風險原始碼重構版
SQL Injection字串拼接 $sqlPDO Prepared Statements
XSS僅 htmlspecialchars 輸出Twig 自動跳脫 + CSP 標頭
CSRF無Token 驗證 + SameSite Cookie
未驗證登入IP 查 user_stusSession + 中介層強制登入
回覆關聯固定欄位 r1~r5parent_id 遞歸結構
編碼問題gb2312、混用全站 UTF-8、資料庫 utf8mb4

擴充建議

  • Markdown 解析:league/commonmark + 安全白名單
  • 圖片上傳:獨立端點、病毒掃描、CDN
  • 提及通知:解析 @username 發送通知
  • 防灌水:頻率限制、內容相似度檢查、Akismet
  • 編輯器:Toast UI Editor / SimpleMDE 整合即時預覽
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved