功能流程
管理員打開寫入頁面 → 填寫 4 組圖片資訊 (原圖/縮圖/說明) + 選擇分類 → 提交表單 → 後端驗證 → 寫入資料庫 → 顯示結果。原始碼採用 `mysql_*` 過時擴展、直接拼接 SQL、無驗證、無 CSRF 防護,本文給出生產級重構版。
重構要點
- PDO + 參數化查詢 徹底防 SQL 注入
- 輸入驗證白名單:分類表名、URL 格式、必填欄位
- CSRF Token:Session 隨機令牌 + `hash_equals()` 驗證
- 統一錯誤處理:Try-catch + 友善提示 + 詳細日誌
- htmlspecialchars 輸出編碼:防 XSS
- utf8mb4 全鏈路:支援 Emoji、生僻字
代碼結構
insert/
├── config.php
├── Database.php
├── WallpaperRepository.php
├── Auth.php
├── create.php # 顯示表單 (GET)
└── store.php # 處理提交 (POST)
核心實作
1. 表單頁 (create.php) — GET
<?php
// create.php
require_once 'config.php';
require_once 'Auth.php';
Auth::startSession();
$config = require 'config.php';
$csrfToken = Auth::generateCsrfToken($config['app']['csrf_token_name']);
?<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>新增桌面圖片記錄</title>
<style>
.form-section { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #eee; }
.form-row { display: grid; grid-template-columns: 140px 1fr; gap: 12px; align-items: center; margin-bottom: 12px; }
.form-row label { font-weight: 500; }
.form-row input[type="url"], .form-row input[type="text"] { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; width: 100%; box-sizing: border-box; }
.required::after { content: " *"; color: #dc3545; }
.btn { padding: 12px 28px; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
.btn-primary { background: #28a745; color: #fff; }
.hint { color: #6c757d; font-size: 0.875rem; margin-top: 4px; }
</style>
</head>
<body style="max-width: 900px; margin: 40px auto; padding: 0 20px;">
<h1>新增桌面圖片記錄</h1>
<p><a href="display.php">← 返回列表</a></p>
<form method="post" action="store.php" novalidate>
<input type="hidden" name="= htmlspecialchars($config['app']['csrf_token_name']) ?>" value="= $csrfToken ?>">
<div class="form-row">
<label class="required">分類</label>
<div>
<label><input type="radio" name="table" value="jpg" checked> 動漫桌布</label>
<label><input type="radio" name="table" value="jpg1"> 水彩畫</label>
<label><input type="radio" name="table" value="jpg2"> 明星寫真</label>
<label><input type="radio" name="table" value="jpg3"> 廣告創意</label>
<label><input type="radio" name="table" value="jpg4"> 其他類</label>
</div>
</div>
<div class="form-section">
<h3>第 1 組圖片 (必填)</h3>
<div class="form-row">
<label class="required">原圖網址</label>
<input type="url" name="url" required placeholder="https://example.com/full.jpg">
</div>
<div class="form-row">
<label class="required">縮圖網址</label>
<input type="url" name="mess" required placeholder="https://example.com/thumb.jpg">
</div>
<div class="form-row">
<label class="required">說明/來源</label>
<input type="text" name="fromx" required maxlength="200" placeholder="圖片描述、作者、來源網站">
</div>
</div>
<div class="form-section">
<h3>第 2 組圖片 (選填)</h3>
<div class="form-row"><label>原圖網址</label><input type="url" name="url1" placeholder="https://..."></div>
<div class="form-row"><label>縮圖網址</label><input type="url" name="mess1" placeholder="https://..."></div>
<div class="form-row"><label>說明/來源</label><input type="text" name="fromx1" maxlength="200"></div>
</div>
<div class="form-section">
<h3>第 3 組圖片 (選填)</h3>
<div class="form-row"><label>原圖網址</label><input type="url" name="url2" placeholder="https://..."></div>
<div class="form-row"><label>縮圖網址</label><input type="url" name="mess2" placeholder="https://..."></div>
<div class="form-row"><label>說明/來源</label><input type="text" name="fromx2" maxlength="200"></div>
</div>
<div class="form-section">
<h3>第 4 組圖片 (選填)</h3>
<div class="form-row"><label>原圖網址</label><input type="url" name="url3" placeholder="https://..."></div>
<div class="form-row"><label>縮圖網址</label><input type="url" name="mess3" placeholder="https://..."></div>
<div class="form-row"><label>說明/來源</label><input type="text" name="fromx3" maxlength="200"></div>
</div>
<button type="submit" class="btn btn-primary">提交儲存</button>
</form>
</body>
</html>
2. 處理提交 (store.php) — POST
<?php
// store.php
require_once 'config.php';
require_once 'Database.php';
require_once 'WallpaperRepository.php';
require_once 'Auth.php';
Auth::startSession();
$config = require 'config.php';
$db = Database::getInstance($config['db']);
$repo = new WallpaperRepository($db->getPdo(), $config['app']['table_prefix'], $config['app']['allowed_tables']);
// CSRF 驗證
$csrfToken = $_POST[$config['app']['csrf_token_name']] ?? '';
if (!Auth::validateCsrfToken($config['app']['csrf_token_name'], $csrfToken)) {
die('CSRF 驗證失敗,請重新提交');
}
// 收集並驗證輸入
$table = $_POST['table'] ?? '';
if (!in_array($table, $config['app']['allowed_tables'], true)) {
die('無效的分類');
}
$required = ['url', 'mess', 'fromx'];
$data = [];
foreach ($required as $field) {
$val = trim($_POST[$field] ?? '');
if ($val === '') {
die("必填欄位 {$field} 不可為空");
}
$data[$field] = $val;
}
// 選填欄位 (1-4 組)
for ($i = 1; $i <= 3; $i++) {
foreach (['url', 'mess', 'fromx'] as $field) {
$key = $field . $i;
if (isset($_POST[$key])) {
$val = trim($_POST[$key]);
if ($val !== '') {
// URL 格式基本驗證
if (str_starts_with($field, 'url') && !filter_var($val, FILTER_VALIDATE_URL)) {
die("{$key} 格式不正確,需為完整網址");
}
$data[$key] = $val;
}
}
}
}
try {
$id = $repo->create($table, $data);
?>
新增成功
✅ 新增成功!自動分配 ID: = $id ?>
| 欄位 | 值 |
$v): ?>
| = htmlspecialchars($k) ?> | = htmlspecialchars($v) ?> |
繼續新增
查看列表
getMessage());
die('系統錯誤,請稍後再試或聯繫管理員');
}
3. Repository 新增方法 (WallpaperRepository.php)
<?php
// WallpaperRepository.php (新增 create 方法)
public function create(string $table, array $data): int {
$this->validateTable($table);
$fields = array_keys($data);
$placeholders = array_map(fn($f) => ":$f", $fields);
$sql = "INSERT INTO `{$this->prefix}{$table}` (" . implode(', ', array_map(fn($f) => "`$f`", $fields)) . ")
VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $this->pdo->prepare($sql);
$stmt->execute($data);
return (int)$this->pdo->lastInsertId();
}
防禦性編程關鍵點總結
| 層面 | 原始碼風險 | 重構對策 |
| 輸入驗證 | 無任何驗證,直接入庫 | 必填檢查、白名單表名、URL 格式驗證、長度限制 |
| SQL 安全 | 字串拼接 `insert into $jpg values (...)` | PDO Prepared Statements + 動態參數綁定 |
| CSRF | 無防護 | Session Token + `hash_equals()` 常時間比較 |
| XSS | 直接輸出變數到 HTML | `htmlspecialchars()` 所有動態輸出 |
| 錯誤處理 | `die("系統出了問題")` 洩漏細節 | Try-catch + 通用訊息 + `error_log()` 記錄堆疊 |
| 編碼 | `charset=gb2312` 不支援完整 Unicode | 資料庫/連線/HTML/JSON 全鏈路 `utf8mb4` |
| 資源管理 | `mysql_connect` 無連線池、無關閉 | PDO 單例模式、自動連線管理 |
擴展建議
總結
p>「數據寫入」是 Web 應用最基礎也最關鍵的環節。重構版展示了 **表單設計、輸入驗證、CSRF 防護、參數化查詢、異常處理、安全輸出** 的完整防禦體系。這套模式適用於所有「建立資源」場景,建議封裝為 `CreateController` 基類,配合表單請求驗證類 (FormRequest) 進一步標準化、減少樣板代碼。