CREATE DATABASE `news_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `news_db`;
CREATE TABLE `news` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`kind` VARCHAR(32) NOT NULL COMMENT '分類:type1/type2/type3/type4',
`title` VARCHAR(120) NOT NULL,
`content` MEDIUMTEXT NOT NULL,
`image` VARCHAR(255) DEFAULT NULL COMMENT '儲存檔名',
`soft_file` VARCHAR(255) DEFAULT NULL COMMENT '軟體附件檔名',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_kind_created` (`kind`, `created_at` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
project/
├── config.php # 資料庫連線設定
├── functions.php # 共用函式
├── insert.php # 新增新聞 (含上傳處理)
├── list.php # 分類列表 + 內容顯示
├── edit.php # 編輯新聞
├── delete.php # 刪除新聞
├── uploads/
│ ├── images/ # chmod 755, www-data 可寫
│ └── soft/ # chmod 755, www-data 可寫
└── templates/ # 視圖模板 (選用)
config.php<?php
// config.php
declare(strict_types=1);
return [
'db' => [
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'news_db',
'user' => 'news_user',
'pass' => 'secure_password',
'charset' => 'utf8mb4',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
'upload' => [
'image_dir' => __DIR__ . '/uploads/images',
'soft_dir' => __DIR__ . '/uploads/soft',
'allowed_image_mime' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
'allowed_soft_mime' => ['application/pdf', 'application/zip', 'application/x-rar-compressed'],
'max_size' => 10 * 1024 * 1024, // 10MB
],
];
functions.php<?php
// functions.php
require_once 'config.php';
$config = require 'config.php';
function getPDO(): PDO {
static $pdo = null;
if ($pdo === null) {
$c = $config['db'];
$dsn = "mysql:host={$c['host']};port={$c['port']};dbname={$c['dbname']};charset={$c['charset']}";
$pdo = new PDO($dsn, $c['user'], $c['pass'], $c['options']);
}
return $pdo;
}
function uploadFile(array $file, string $targetDir, array $allowedMime, int $maxSize): ?string {
if ($file['error'] !== UPLOAD_ERR_OK) return null;
if ($file['size'] > $maxSize) throw new Exception('檔案過大');
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowedMime, true)) throw new Exception('檔案類型不允許');
$ext = match ($mime) {
'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp',
'application/pdf' => 'pdf', 'application/zip' => 'zip', 'application/x-rar-compressed' => 'rar',
default => 'bin',
};
$filename = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = $targetDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new Exception('檔案移動失敗');
}
return $filename;
}
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
insert.php<?php
// insert.php
require_once 'functions.php';
$config = require 'config.php';
$pdo = getPDO();
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$title = trim($_POST['title'] ?? '');
$kind = $_POST['kind'] ?? '';
$content = $_POST['content'] ?? '';
// 驗證
if ($title === '') $errors[] = '標題不可為空';
if (!in_array($kind, ['type1','type2','type3','type4'], true)) $errors[] = '分類錯誤';
if ($content === '') $errors[] = '內容不可為空';
// 上傳處理
$imageName = null;
$softName = null;
if (!empty($_FILES['image']['name'])) {
$imageName = uploadFile($_FILES['image'], $config['upload']['image_dir'],
$config['upload']['allowed_image_mime'], $config['upload']['max_size']);
}
if (!empty($_FILES['soft']['name'])) {
$softName = uploadFile($_FILES['soft'], $config['upload']['soft_dir'],
$config['upload']['allowed_soft_mime'], $config['upload']['max_size']);
}
if (!$errors) {
$stmt = $pdo->prepare(
'INSERT INTO news (kind, title, content, image, soft_file) VALUES (?, ?, ?, ?, ?)'
);
$stmt->execute([$kind, $title, $content, $imageName, $softName]);
$success = true;
// 重導向防止重複送出
header('Location: list.php?kind=' . urlencode($kind) . '&msg=added');
exit;
}
} catch (Exception $e) {
$errors[] = '系統錯誤:' . $e->getMessage();
}
}
// 顯示表單 (可改用模板引擎)
?>
<!DOCTYPE html>
<html lang="zh-TW">
<head><meta charset="UTF-8"><title>新增新聞</title></head>
<body>
<h1>新增新聞</h1>
<?php if ($errors): ?><ul style="color:red"><?php foreach ($errors as $e): ?><li><?= h($e) ?></li><?php endforeach; ?></ul><?php endif; ?>
<form method="POST" enctype="multipart/form-data">
<label>分類:
<select name="kind" required>
<option value="type1">類型 1</option>
<option value="type2">類型 2</option>
<option value="type3">類型 3</option>
<option value="type4">類型 4</option>
</select>
</label><br><br>
<label>標題:<input type="text" name="title" size="60" required></label><br><br>
<label>內容:<br><textarea name="content" cols="60" rows="10" required></textarea></label><br><br>
<label>圖片:<input type="file" name="image" accept="image/*"></label><br><br>
<label>軟體附件:<input type="file" name="soft" accept=".pdf,.zip,.rar"></label><br><br>
<button type="submit">送出</button>
</form>
</body></html>
list.php<?php
// list.php
require_once 'functions.php';
$pdo = getPDO();
$kind = $_GET['kind'] ?? 'type1';
$id = $_GET['id'] ?? null;
// 取得分類列表 (最新 10 筆)
$stmt = $pdo->prepare('SELECT id, title FROM news WHERE kind = ? ORDER BY created_at DESC LIMIT 10');
$stmt->execute([$kind]);
$list = $stmt->fetchAll();
// 決定顯示哪一篇
if ($id === null && $list) {
$id = $list[0]['id'];
}
$article = null;
if ($id !== null) {
$stmt = $pdo->prepare('SELECT * FROM news WHERE id = ? AND kind = ?');
$stmt->execute([$id, $kind]);
$article = $stmt->fetch();
}
?>
<!DOCTYPE html>
<html lang="zh-TW">
<head><meta charset="UTF-8"><title>新聞列表 - <?= h($kind) ?></title></head>
<body>
<h1><?= h($kind) ?> 最新消息</h1>
<div style="display:flex; gap:2rem;">
<aside style="width:300px;">
<h3>文章列表</h3>
<ul>
<?php foreach ($list as $item): ?>
<li><a href="?kind=<?= h($kind) ?>&id=<?= $item['id'] ?>" <?= $item['id']==$id?'style="font-weight:bold;"':'' ?>><?= h($item['title']) ?></a></li>
<?php endforeach; ?>
</ul>
</aside>
<main style="flex:1;">
<?php if ($article): ?>
<h2><?= h($article['title']) ?></h2>
<small>發布時間:<?= h($article['created_at']) ?></small><hr>
<?php if ($article['image']): ?>
<img src="/uploads/images/<?= h($article['image']) ?>" style="max-width:100%; float:right; margin-left:1rem;" alt="">
<?php endif; ?>
<div><?= nl2br(h($article['content'])) ?></div>
<?php if ($article['soft_file']): ?>
<p style="text-align:right; margin-top:2rem;">
<a href="/uploads/soft/<?= h($article['soft_file']) ?>" download>⬇ 下載附件:<?= h($article['soft_file']) ?></a>
</p>
<?php endif; ?>
<?php else: ?>
<p>無文章內容</p>
<?php endif; ?>
</main>
</div>
</body></html>
| 原教學碼問題 | 本文改進方案 |
|---|---|
使用 mysql_* 已移除函式 | 使用 PDO + Prepared Statements |
| SQL 直接拼接變數 → SQL Injection | 參數綁定 execute([...]) |
| 上傳檔名直接用原檔名 → 覆蓋/注入風險 | 隨機檔名 + MIME 驗證 + 副檔名白名單 |
| 無輸出轉義 → XSS | 統一 h() 輔助函式 + nl2br() |
| 目錄權限 777 | 755 + 擁有者 www-data,僅必要目錄可寫 |
| 硬編碼 DB 憑證 | 設定檔分離,不納入版控 |
錯誤處理僅 die() | try-catch + 友善錯誤訊息 + 記錄 Log |
mkdir -p uploads/images uploads/soft && chown www-data:www-data uploads/* && chmod 755 uploads/*php.ini:upload_max_filesize, post_max_size, max_execution_timeuploads/ 執行 PHP本文展示了從資料庫設計、檔案上傳、CRUD 操作到前端顯示的完整新聞分類系統。核心原則:**參數化查詢防注入、輸出轉義防 XSS、檔案上傳嚴格驗證、設定與程式碼分離**。此架構易於擴充:加入分頁、搜尋、標籤、權限控制、API 介面等功能。