論壇系統中最核心的閱讀體驗,由「主題顯示」、「回文列表」、「分頁導航」、「上/下主題跳轉」以及「回文/引用表單」構成。本文將這些功能模組化、現代化重構,修正原始碼中的 XSS 風險、HTML 字串拼接混亂、邏輯耦合等問題。
在同一版面 (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 欄位在寫入時更新(空間換時間)。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;
}
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":防止反向標籤釣魚攻擊。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;
}
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;
}
// 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)、參數化查詢、異常處理 等現代安全規範。這樣的架構不僅易於維護,也為後續引入模板引擎、前後端分離奠定基礎。