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

經典 PHP 專案實戰:杏林同學錄(七)—— 留言簿分頁、發文與權限控制

     編輯:PHP 綜合知識

模組定位:班級專屬的輕量級 BBS

「杏林同學錄」留言簿(class/notebook/)僅對已登入班級成員開放,提供「分頁瀏覽、發文、刪除管理」三大核心功能。程式碼採用Session 驗證守衛、LIMIT 分頁、nl2br(strip_tags()) 內容淨化、IP 記錄等經典模式,是研究「小型社區互動核心」的優良教材。

入口守衛:index.php / addnote.php 共用 Session 檢查

<?php
session_start();
if (!isset($_SESSION['userregister']) || empty($_SESSION['userregister'])) {
    header('Location: ../index.php'); // 導向前台登入
    exit;
}
$user = $_SESSION['userregister']; // 當前登入用戶名
?>

現代化建議:抽離為 AuthMiddleware 或 require_auth() 函數,支援 AJAX 請求回傳 401 JSON、頁面請求重導並保留 redirect_url。

分頁顯示邏輯 index.php 逐行解析

原始分頁算法

$result = mysql_query("SELECT * FROM notebook", $db);
$max = mysql_num_rows($result);          // 總筆數(全表掃描,效能差)
$page = $_GET['page'] ?? 1;
$perPage = 10;
$p = ceil($max / $perPage);              // 總頁數
$low = $perPage * ($page - 1);           // OFFSET
$x = ($page == $p && $max % $perPage) ? $max % $perPage : $perPage; // 最後一頁筆數
if ($max == 0) $x = 0;

$result = mysql_query("SELECT * FROM notebook ORDER BY time DESC LIMIT $low, $x", $db);

效能與安全優化

問題影響現代方案
SELECT * 兩次查詢全表掃描兩次,大數據極慢SELECT COUNT(*) + SELECT id, user, time, title, nnote ... 只查需要欄位
LIMIT $low, $x 直接拼接SQL 注入(雖 $low/$x 經運算,但不保險)PDO LIMIT :offset, :limit 繫結參數
每則留言額外查詢用戶表N+1 問題:10 則留言 = 11 次查詢JOIN 查詢或批次載入 WHERE user IN (...)
時間排序無索引檔案排序加索引 KEY idx_time (time DESC)

優化後單查詢(JOIN)

$stmt = $pdo->prepare("
    SELECT n.id, n.user, n.time, n.ip, n.title, n.nnote,
           u.name, u.signature, u.email, u.face
    FROM notebook n
    JOIN users u ON u.username = n.user
    ORDER BY n.time DESC
    LIMIT :offset, :limit
");
$stmt->bindValue(':offset', ($page-1)*$perPage, PDO::PARAM_INT);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->execute();
$notes = $stmt->fetchAll(PDO::FETCH_ASSOC);

// 總數查詢(可快取 30 秒)
$total = $pdo->query("SELECT COUNT(*) FROM notebook")->fetchColumn();

留言渲染:從 echo 拼接到模板引擎

原始碼在迴圈中大量 echo "<table...>",混雜變數插值、樣式類別、圖片路徑。現代 Twig 範例:

{# notes_list.twig #}
{% for note in notes %}
<article class="note-card">
  <header class="note-header">
    <img src="{{ asset('images/face/icon'~note.face~'.gif') }}" width="32" height="32" class="avatar">
    <strong>{{ note.name }}</strong>
    <time datetime="{{ note.time|date('c') }}">{{ note.time|date('Y-m-d H:i') }}</time>
    <a href="mailto:{{ note.email }}" title="發送郵件"><img src="{{ asset('images/email.gif') }}" alt="Email"></a>
    <span class="ip" title="發文 IP">{{ note.ip }}</span>
  </header>
  <h3 class="note-title">{{ note.title }}</h3>
  <div class="note-content">{{ note.nnote|raw }}</div> {# 已經過 nl2br(strip_tags) #}
  <footer class="note-signature">{{ note.signature|nl2br }}</footer>
</article>
{% else %}
<p class="empty">目前沒有留言,搶個沙發吧!</p>
{% endfor %}

注意:nnote 已在寫入時做 nl2br(strip_tags()),故此處可用 |raw;若改為儲存原始文字,模板需自行 |nl2br|escape。

分頁導航元件化

{# pagination.twig #}

發文處理 addnote.php

原始流程

    1. Session 守衛(同 index.php) 2. POST 提交 → $time = date('Y年m月d日 H:i:s A')、$ip = $REMOTE_ADDR 3. $title = strip_tags($top)、$nnote = nl2br(strip_tags($content)) 4. 必填檢查 → INSERT INTO notebook (user, time, ip, title, nnote) VALUES ... 5. 成功提示「留言成功!」

改進要點

  • CSRF Token:表單隱藏欄位 _token,驗證 $_POST['_token'] === $_SESSION['_token']。
  • 內容淨化策略升級:strip_tags() 會殘留屬性(如 onclick),建議使用 HTMLPurifier 白名單模式,或採用 Markdown + Parsedown(禁止 HTML)。
  • IP 獲取:$REMOTE_ADDR 在反向代理下不準,需讀取 $_SERVER['HTTP_X_FORWARDED_FOR'](信任代理清單)。
  • 防灌水:同用戶 30 秒內限發 1 則、關鍵詞過濾、新用戶發文審核。
  • PRG 模式:成功後 header('Location: index.php?page=1') 而非直接輸出文字,避免重新整理重複提交。
  • 錯誤處理:try-catch 包裝 PDO,失敗記錄日誌、回顯表單並保留輸入(old())。

重構後控制器骨架

<?php
// addnote.php
session_start();
require __DIR__.'/../config.php'; // PDO $pdo
require __DIR__.'/../lib/csrf.php'; // validate_csrf()

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    validate_csrf(); // 失敗拋出異常
    $title = trim($_POST['title'] ?? '');
    $content = trim($_POST['content'] ?? '');
    $errors = [];
    if ($title === '') $errors['title'] = '標題不可為空';
    if ($content === '') $errors['content'] = '內容不可為空';
    if (mb_strlen($title) > 80) $errors['title'] = '標題過長';
    if (mb_strlen($content) > 5000) $errors['content'] = '內容過長';

    if (!$errors) {
        try {
            $ip = getClientIp(); // 處理代理
            $safeContent = nl2br(htmlspecialchars($content, ENT_QUOTES)); // 或 HTMLPurifier
            $stmt = $pdo->prepare("INSERT INTO notebook (user, time, ip, title, nnote) VALUES (?, NOW(), ?, ?, ?)");
            $stmt->execute([$_SESSION['userregister'], $ip, $title, $safeContent]);
            header('Location: index.php?page=1&posted=1');
            exit;
        } catch (PDOException $e) {
            error_log($e->getMessage());
            $errors['db'] = '系統繁忙,請稍後再試';
        }
    }
    // 失敗:回顯表單
    $_SESSION['old'] = ['title' => $title, 'content' => $content];
    $_SESSION['errors'] = $errors;
    header('Location: addnote.php');
    exit;
}

// GET:顯示表單
$old = $_SESSION['old'] ?? ['title'=>'', 'content'=>''];
$errors = $_SESSION['errors'] ?? [];
unset($_SESSION['old'], $_SESSION['errors']);
?>
<!DOCTYPE html>... <form method="post"> {{ csrf_field() }} ... </form>

資料庫結構優化

CREATE TABLE `notebook` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user` VARCHAR(32) NOT NULL,                 -- 關聯 users.username
  `title` VARCHAR(120) NOT NULL,
  `content` MEDIUMTEXT NOT NULL,               -- 儲存已淨化 HTML 或原始 Markdown
  `ip` VARBINARY(16) NOT NULL,                 -- IPv6 相容,INET6_ATON 儲存
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `deleted_at` DATETIME DEFAULT NULL,          -- 軟刪除
  PRIMARY KEY (`id`),
  KEY `idx_user_time` (`user`, `created_at`),
  KEY `idx_time` (`created_at`),
  CONSTRAINT `fk_note_user` FOREIGN KEY (`user`) REFERENCES `users`(`username`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

管理功能:刪除留言 delnote.php(原始碼未給出,補充設計)

  • 權限:作者本人、班級幹部、超級管理員可刪除。
  • 方式:軟刪除 UPDATE notebook SET deleted_at=NOW() WHERE id=?。
  • 介面:列表頁每則留言顯示「刪除」按鈕(僅有權限者可見),JS 確認 → POST /notebook/delete → 回傳 JSON → 前端移除 DOM。
  • 日誌:記錄 admin_log (operator, action, target_id, reason, ip, time)。

本章總結

「杏林同學錄」留言簿雖小,卻包含認證守衛、分頁查詢、N+1 優化、內容淨化、CSRF 防護、PRG 模式、IP 處理、軟刪除、權限控制等 Web 社區核心要素。重構路徑清晰:

  1. 引入 PDO + 預處理 + 事務 替換 mysql_*。
  2. 採用 中介層統一認證/CSRF/速率限制。
  3. 使用 模板引擎 分離視圖,支援繼承、區塊、過濾器。
  4. 導入 HTMLPurifier / Markdown 統一內容安全策略。
  5. 補充 單元/功能/E2E 測試,覆蓋發文、分頁、刪除、權限邊界。

至此,「杏林同學錄」系列解析完結。從兩階段註冊、管理後台、留言簿,我們看到了一個完整的封閉社群平台在 PHP 4/5 時代的實現縮影,也梳理出通往現代 Laravel/Symfony/Api Platform 架構的遷移階梯。願這些經典代碼啟發你在新技術棧上構建更安全、可維護、可擴展的應用。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved