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

社區論壇 PHP & MySQL 實戰(四):閱讀頁面、分頁導航與回文表單完整實作

     編輯:PHP 綜合知識

前言

論壇系統中最核心的閱讀體驗,由「主題顯示」、「回文列表」、「分頁導航」、「上/下主題跳轉」以及「回文/引用表單」構成。本文將這些功能模組化、現代化重構,修正原始碼中的 XSS 風險、HTML 字串拼接混亂、邏輯耦合等問題。

核心模組一:上/下主題導航 — prev/next_subj()

在同一版面 (aid) 內,按發文時間 (wdate) 找尋相鄰主題。

function getAdjacentSubjects(PDO $pdo, int $aid, int $currentSid): array
{
    // 取得同版面所有主題 ID,按時間排序
    $stmt = $pdo->prepare("SELECT id FROM bbs_subj WHERE aid = :aid ORDER BY wdate");
    $stmt->execute([':aid' => $aid]);
    $ids = $stmt->fetchAll(PDO::FETCH_COLUMN);

    $currentIndex = array_search($currentSid, $ids);
    if ($currentIndex === false) return ['prev' => null, 'next' => null];

    return [
        'prev' => $currentIndex > 0 ? $ids[$currentIndex - 1] : null,
        'next' => $currentIndex < count($ids) - 1 ? $ids[$currentIndex + 1] : null,
    ];
}

優化: 原始碼每次查詢都撈全表 ID 陣列,大版面極慢。建議:

  • 使用 SELECT id FROM bbs_subj WHERE aid = :aid AND wdate < :current_wdate ORDER BY wdate DESC LIMIT 1 直接取上一筆。
  • 或維護 prev_id, next_id 欄位在寫入時更新(空間換時間)。

核心模組二:導航列輸出 — viewbar()

function renderViewbar(string $selfUrl, int $aid, int $sid, array $adjacent): string
{
    $html = '<nav class="viewbar"><ul>';
    $html .= '<li><a href="' . $selfUrl . "?aid={$aid}&sid={$sid}\">[回目錄]</a></li>';
    $html .= '<li><a href="#reply">[我要回文]</a></li>';
    $html .= '<li><a href="' . $selfUrl . "?aid={$aid}\">[最新主題]</a></li>';

    if ($adjacent['next']) {
        $html .= '<li><a href="' . $selfUrl . "?aid={$aid}&sid={$adjacent['next']}&rid={$adjacent['next']}\">[下一主題]</a></li>';
    }
    if ($adjacent['prev']) {
        $html .= '<li><a href="' . $selfUrl . "?aid={$aid}&sid={$adjacent['prev']}&rid={$adjacent['prev']}\">[上一主題]</a></li>';
    }
    $html .= '</ul></nav>';
    return $html;
}

核心模組三:主題與回文顯示 — view()

function renderThreadView(PDO $pdo, string $selfUrl, int $aid, int $sid, int $rid): array
{
    // 1. 判斷是主題還是回文,並增加瀏覽數
    $isSubject = ($sid === $rid);
    $table = $isSubject ? 'bbs_subj' : 'bbs_docs';

    $pdo->prepare("UPDATE {$table} SET views = views + 1 WHERE id = :rid")
        ->execute([':rid' => $rid]);

    // 2. 讀取版面標題
    $areaStmt = $pdo->prepare("SELECT banner FROM bbs_area WHERE id = :aid");
    $areaStmt->execute([':aid' => $aid]);
    $areaBanner = $areaStmt->fetchColumn() ?? '未知版面';

    // 3. 讀取內容
    $stmt = $pdo->prepare("SELECT poster, pip, wdate, banner, body FROM {$table} WHERE id = :rid");
    $stmt->execute([':rid' => $rid]);
    $post = $stmt->fetch();
    if (!$post) throw new Exception('內容不存在');

    // 4. 內容安全處理:HTML 轉義 + 自動連結
    $body = htmlspecialchars($post['body'], ENT_QUOTES, 'UTF-8');
    $body = nl2br($body); // 換行轉 
// 簡易自動連結 (僅示意,生產用成熟庫如 league/url-highlighter) $body = preg_replace_callback( '~(https?://\S+)~', fn($m) => '<a href="' . htmlspecialchars($m[1]) . '" target="_blank" rel="noopener">' . htmlspecialchars($m[1]) . '</a>', $body ); // 5. 組裝輸出資料 $wdate = date('m/d H:i', strtotime($post['wdate'])); return [ 'area_banner' => $areaBanner, 'subject_banner' => $post['banner'], 'poster' => htmlspecialchars($post['poster']), 'ip' => $post['pip'], 'wdate' => $wdate, 'body' => $body, 'viewbar_top' => renderViewbar($selfUrl, $aid, $sid, getAdjacentSubjects($pdo, $aid, $sid)), 'is_subject' => $isSubject, ]; }

關鍵安全處理

  • htmlspecialchars(..., ENT_QUOTES, 'UTF-8'):防止 XSS,將使用者輸入的 HTML 標籤實體化顯示。
  • nl2br():保留換行視覺效果。
  • 自動連結使用 preg_replace_callback 配合 htmlspecialchars,避免注入惡意屬性。
  • target="_blank" rel="noopener":防止反向標籤釣魚攻擊。

核心模組四:回文/引用表單 — renderReplyForm()

function renderReplyForm(string $selfUrl, int $aid, int $sid, int $type, string $rebanner = ''): string
{
    // 自動加 Re: 前綴
    if ($type == 1 && !preg_match('/^Re:/', $rebanner)) {
        $rebanner = 'Re: ' . $rebanner;
    }

    $html = '<form action="' . $selfUrl . '?proc=post2" method="post" class="reply-form">';
    $html .= '<input type="hidden" name="aid" value="' . $aid . '">';
    $html .= '<input type="hidden" name="sid" value="' . $sid . '">';
    $html .= '<input type="hidden" name="type" value="' . $type . '">';
    $html .= '<table>';

    if ($sid) {
        $html .= '<tr><th colspan="2">回覆主題</th></tr>';
    }

    $html .= '<tr><th>標題:</th><td><input type="text" name="banner" size="60" value="' . htmlspecialchars($rebanner) . '"></td></tr>';
    $html .= '<tr><th>暱稱:</th><td><input type="text" name="poster" size="30" value=""></td></tr>';
    $html .= '<tr><th>E-Mail:</th><td><input type="text" name="email" size="30" value=""></td></tr>';
    $html .= '<tr><th valign="top">內容:</th><td>';
    $html .= '<textarea name="body" cols="80" rows="16"></textarea>';
    $html .= '<small>支援自動連結: 輸入 https://example.com 將自動轉為超連結</small>';
    $html .= '</td></tr>';

    if ($sid) {
        $html .= '<tr><td colspan="2" align="center">';
        $html .= '<input type="submit" value="送出">';
        $html .= '</td></tr>';
    }

    $html .= '</table></form>';
    return $html;
}

核心模組五:分頁導航 — pagebar() (列表頁用)

function renderPagebar(string $selfUrl, int $aid, int $currentPage, int $totalPages): string
{
    if ($totalPages <= 1) return '';

    $html = '<nav class="pagination"><ul>';
    if ($currentPage > 1) {
        $html .= '<li><a href="' . $selfUrl . "?aid={$aid}&pgno=" . ($currentPage - 1) . '">« 上一頁</a></li>';
    }
    for ($i = 1; $i <= $totalPages; $i++) {
        if ($i == $currentPage) {
            $html .= '<li class="active"><span>' . $i . '</span></li>';
        } else {
            $html .= '<li><a href="' . $selfUrl . "?aid={$aid}&pgno={$i}\">{$i}</a></li>';
        }
    }
    if ($currentPage < $totalPages) {
        $html .= '<li><a href="' . $selfUrl . "?aid={$aid}&pgno=" . ($currentPage + 1) . '">下一頁 »</a></li>';
    }
    $html .= '</ul></nav>';
    return $html;
}

整合輸出:Controller 層範例

// view.php (單一入口分派)
$aid = (int)($_GET['aid'] ?? 1);
$sid = (int)($_GET['sid'] ?? 0);
$rid = (int)($_GET['rid'] ?? 0);
$pgno = (int)($_GET['pgno'] ?? 1);
$proc = $_GET['proc'] ?? 'area';

try {
    $pdo = getPdoConnection(); // 從 sql.php 取得 PDO 實例

    switch ($proc) {
        case 'view':
            if (!$sid || !$rid) throw new Exception('參數不足');
            $data = renderThreadView($pdo, $_SERVER['PHP_SELF'], $aid, $sid, $rid);
            include 'templates/view.php'; // 模板檔只負責 echo $data['body'] 等
            break;
        case 'area':
            // 列表頁邏輯:計算總頁數、撈取當頁資料、renderPagebar()...
            break;
        // ...
    }
} catch (Exception $e) {
    http_response_code(500);
    include 'templates/error.php';
    exit;
}

小結

本文將閱讀頁面拆解為五大可複用模組(鄰近導航、導航列、內容渲染、回文表單、分頁),採用 資料處理與視圖分離 原則,並嚴格實作 輸出編碼 (htmlspecialchars)、參數化查詢、異常處理 等現代安全規範。這樣的架構不僅易於維護,也為後續引入模板引擎、前後端分離奠定基礎。

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