Add files via upload

This commit is contained in:
zhang-pu 2026-06-01 09:48:36 +08:00 committed by GitHub
commit fdeba3dbbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 711 additions and 0 deletions

46
CHANGELOG.md Normal file
View file

@ -0,0 +1,46 @@
# 更新日志
所有版本更新记录均在此文件更新。
---
## [v1.2.0] - 2024-05-31
### 定时任务 + 远程备份
- ⏰ 完整的定时任务管理(创建/删除/启用/禁用/立即执行)
- ✅ Cron 表达式格式验证(分/时/日/月/周五段校验)
- 💾 远程 rsync 备份(SSH 主机/端口/用户/密钥)
- 🔗 远程连接测试工具(部署前验证连通性)
- 📊 备份统计面板(总数/本地大小/远程大小/磁盘占用)
- 📁 文件管理:浏览/上传/编辑/删除/新建目录/权限显示
- 🔐 SSL 证书列表(到期天数/状态指示灯)
- 🔄 SSL 单个续期 + 批量续期
- 🛡️ 自动漏洞修复(每日凌晨3点执行)
---
## [v1.0.0] - 2024-05-31
### 首发版本
- 🚀 完整的博客系统,包含文章发布、分类管理、评论系统
- 🎨 双主题支持:Flow(浅色)和 Magine(深色)
- 🔐 安全特性:CSRF 防护、XSS 过滤、bcrypt 密码哈希、评论速率限制
- 📱 响应式设计,支持移动端访问
- ✏️ 富文本编辑器,支持 Markdown 快捷键
- 🛡️ 垃圾评论过滤(蜜罐 + 关键词检测)
- ⚙️ 安装向导,开箱即用
---
## 待办
- [ ] 多 PHP 版本切换
- [ ] Node.js 支持
- [ ] nginx 日志查看器
- [ ] 插件系统
---
> 关注 GitHub 仓库获取最新更新:https://github.com/tblog-cn/tblog

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 TBlog
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

118
README.md Normal file
View file

@ -0,0 +1,118 @@
# TBlog
一个简洁、优雅的开源博客系统,基于 PHP + MySQL 开发。
🌐 网址:https://tblog.cn
---
## 特性
- ✏️ 富文本编辑器,写作体验流畅
- 🎨 两套主题可选(Flow 浅色 / Magine 深色)
- 💬 评论系统,支持垃圾评论过滤
- 📁 分类管理,文章归档
- 🔐 安全防护:CSRF 防护、XSS 过滤、密码 bcrypt 哈希、评论速率限制
- 📱 响应式布局,适配移动端
- 🛠 轻量高效,无需复杂依赖
---
## 环境要求
- PHP >= 7.0
- MySQL >= 5.6
- mysqli 扩展
- PDO 扩展(可选)
---
## 安装
### 方式一:向导安装
1. 解压源码,上传至网站目录
2. 访问 `http://你的域名/install.php`
3. 按提示填写数据库信息,完成安装
4. 初始管理员账号:`admin` / `admin123`(安装后请立即修改)
### 方式二:手动配置
1. 创建数据库(例如 `tblog`)
2. 导入 `install.sql` 建表
3. 复制并重命名 `config.php.new` 为 `config.php`,填入数据库信息
4. 通过 SQL 设置管理员密码哈希:
```sql
INSERT INTO settings (`key`, value) VALUES ('admin_user', 'admin');
INSERT INTO settings (`key`, value) VALUES ('admin_hash', '$2y$10$YOUR_HASH_HERE');
```
生成哈希:PHP 执行 `echo password_hash('你的密码', PASSWORD_DEFAULT);`
---
## 目录结构
```
tblog/
├── admin/ 管理后台
│ ├── assets/ 样式资源
│ ├── category.php 分类管理
│ ├── comment.php 评论管理
│ ├── index.php 管理概览
│ ├── login.php 登录页
│ ├── logout.php 退出
│ ├── post.php 文章管理
│ └── settings.php 系统设置
├── include/ 核心类库
│ ├── db.php 数据库连接
│ ├── router.php URL 路由
│ ├── function.common.php
│ ├── function.post.php
│ ├── function.category.php
│ ├── function.comment.php
│ └── HTMLPurifier.simple.php HTML 净化
├── static/ 静态资源
├── templates/ 主题模板
│ ├── flow/ Flow 主题(浅色)
│ └── magine/ Magine 主题(深色)
├── index.php 入口文件
├── config.php.new 配置文件模板
├── install.php 安装向导
└── install.sql 数据库建表脚本
```
---
## 管理后台
访问 `http://你的域名/admin/`,使用安装时设置的管理员账号登录。
---
## 主题切换
文章可独立选择模板(Flow / Magine),全站默认模板可在后台「设置」中修改。
---
## 开源协议
MIT License
---
## 更新日志
详见 [CHANGELOG.md](./CHANGELOG.md)
---
## 问题反馈
如有 Bug 或建议,欢迎提交 Issue。
---
**TBlog** — 简洁而不简单。

18
config.php.new Normal file
View file

@ -0,0 +1,18 @@
<?php
/**
* TBlog - Configuration Template
* 复制此文件为 config.php 并填写数据库信息
*/
define('DB_HOST', 'localhost');
define('DB_PORT', 3306);
define('DB_USER', 'your_db_user');
define('DB_PASS', 'your_db_password');
define('DB_NAME', 'tblog');
define('DB_PREFIX', '');
// Path constants
define('ROOT_PATH', __DIR__ . '/');
define('INCLUDE_PATH', ROOT_PATH . 'include/');
define('TEMPLATE_PATH', ROOT_PATH . 'templates/');
session_start();

18
index.php Normal file
View file

@ -0,0 +1,18 @@
<?php
/**
* ZhangPu Blog - Main Entry Point
*/
// Load configuration
require_once __DIR__ . '/config.php';
// Load functions
require_once INCLUDE_PATH . 'function.common.php';
require_once INCLUDE_PATH . 'router.php';
// Get the route from URL
$route = isset($_GET['route']) ? $_GET['route'] : '';
$route = trim($route, '/');
// Route the request
route_request($route);

432
install.php Normal file
View file

@ -0,0 +1,432 @@
<?php
/**
* TBlog - Installation Wizard
*/
session_start();
// Load common functions
require_once __DIR__ . '/include/function.common.php';
$step = isset($_GET['step']) ? intval($_GET['step']) : 1;
$error = '';
$success = '';
// Handle form submission for step 2
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $step === 2) {
$_SESSION['db_host'] = trim($_POST['db_host'] ?? 'localhost');
$_SESSION['db_port'] = intval($_POST['db_port'] ?? 3306);
$_SESSION['db_user'] = trim($_POST['db_user'] ?? '');
$_SESSION['db_pass'] = trim($_POST['db_pass'] ?? '');
$_SESSION['db_name'] = trim($_POST['db_name'] ?? '');
// Test connection
$mysqli = @new mysqli($_SESSION['db_host'], $_SESSION['db_user'], $_SESSION['db_pass'], '', $_SESSION['db_port']);
if ($mysqli->connect_error) {
$error = '数据库连接失败: ' . $mysqli->connect_error;
} else {
// Create database if not exists
$mysqli->query("CREATE DATABASE IF NOT EXISTS `{$_SESSION['db_name']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$mysqli->select_db($_SESSION['db_name']);
// Read and execute install.sql
$sql_file = file_get_contents(__DIR__ . '/install.sql');
$statements = array_filter(array_map('trim', explode(';', $sql_file)));
foreach ($statements as $statement) {
if (!empty($statement)) {
if (!$mysqli->query($statement)) {
$error = '数据库初始化失败: ' . $mysqli->error;
break;
}
}
}
if (!$error) {
$success = '数据库连接成功,配置已保存';
header('Location: ?step=3');
exit;
}
$mysqli->close();
}
}
// Handle step 3 - save config and set admin credentials
if ($step === 3) {
$config_content = "<?php
/**
* TBlog - Configuration
*/
define('DB_HOST', '{$_SESSION['db_host']}');
define('DB_PORT', {$_SESSION['db_port']});
define('DB_USER', '" . addslashes($_SESSION['db_user']) . "');
define('DB_PASS', '" . addslashes($_SESSION['db_pass']) . "');
define('DB_NAME', '{$_SESSION['db_name']}');
define('DB_PREFIX', '');
// Path constants
define('ROOT_PATH', __DIR__ . '/');
define('INCLUDE_PATH', ROOT_PATH . 'include/');
define('TEMPLATE_PATH', ROOT_PATH . 'templates/');
// Admin credentials stored in database (settings table)
// Initial setup below; change via admin panel after install
session_start();
";
if (!file_put_contents(__DIR__ . '/config.php', $config_content)) {
$error = '配置文件写入失败,请检查目录权限';
} else {
// Set initial admin credentials in database
$mysqli = @new mysqli($_SESSION['db_host'], $_SESSION['db_user'], $_SESSION['db_pass'], $_SESSION['db_name'], $_SESSION['db_port']);
if (!$mysqli->connect_error) {
$admin_user = 'admin';
$admin_hash = password_hash('admin123', PASSWORD_DEFAULT);
$mysqli->query("INSERT INTO settings (`key`, value) VALUES ('admin_user', 'admin') ON DUPLICATE KEY UPDATE value = 'admin'");
$mysqli->query("INSERT INTO settings (`key`, value) VALUES ('admin_hash', '$admin_hash') ON DUPLICATE KEY UPDATE value = '$admin_hash'");
$mysqli->close();
}
header('Location: ?step=4');
exit;
}
}
// Step 5 - completion
if ($step === 5) {
session_destroy();
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安装向导 - TBlog</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #22c55e 0%, #0ea5e9 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.installer {
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
width: 100%;
max-width: 600px;
overflow: hidden;
}
.installer-header {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: #fff;
padding: 30px;
text-align: center;
}
.installer-header h1 {
font-size: 28px;
margin-bottom: 8px;
}
.installer-header p {
opacity: 0.9;
}
.progress-bar {
display: flex;
padding: 0 30px;
margin-top: 20px;
}
.progress-step {
flex: 1;
height: 4px;
background: #e5e5e5;
position: relative;
}
.progress-step.completed {
background: #22c55e;
}
.progress-step.active {
background: #22c55e;
}
.progress-step::before {
content: attr(data-step);
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 24px;
background: #e5e5e5;
border-radius: 50%;
font-size: 12px;
line-height: 24px;
color: #666;
}
.progress-step.completed::before,
.progress-step.active::before {
background: #22c55e;
color: #fff;
}
.installer-body {
padding: 40px 30px;
}
h2 {
color: #1a1a2e;
margin-bottom: 20px;
font-size: 22px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
input[type="text"],
input[type="password"],
input[type="number"] {
width: 100%;
padding: 14px 16px;
border: 2px solid #e5e5e5;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #22c55e;
}
.btn {
display: inline-block;
padding: 14px 32px;
background: #22c55e;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
text-decoration: none;
}
.btn:hover {
background: #16a34a;
}
.btn-secondary {
background: #e5e5e5;
color: #333;
}
.btn-secondary:hover {
background: #d5d5d5;
}
.error {
background: #fee2e2;
color: #dc2626;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.success {
background: #dcfce7;
color: #16a34a;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
}
.requirements {
list-style: none;
}
.requirements li {
padding: 12px 0;
border-bottom: 1px solid #e5e5e5;
display: flex;
align-items: center;
gap: 10px;
}
.requirements li:last-child {
border-bottom: none;
}
.requirements .ok { color: #22c55e; }
.requirements .fail { color: #dc2626; }
.buttons {
display: flex;
gap: 12px;
margin-top: 30px;
}
.features {
background: #f9fafb;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.features ul {
list-style: none;
}
.features li {
padding: 8px 0;
padding-left: 24px;
position: relative;
}
.features li::before {
content: '✓';
position: absolute;
left: 0;
color: #22c55e;
}
.info-box {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
color: #1e40af;
}
.text-center { text-align: center; }
.mt-4 { margin-top: 24px; }
</style>
</head>
<body>
<div class="installer">
<div class="installer-header">
<h1>🌿 TBlog 安装向导</h1>
<p>一个简洁优雅的博客系统</p>
<div class="progress-bar">
<div class="progress-step <?php echo $step >= 1 ? 'completed' : ''; ?>" data-step="1"></div>
<div class="progress-step <?php echo $step >= 2 ? 'completed' : ''; ?> <?php echo $step == 2 ? 'active' : ''; ?>" data-step="2"></div>
<div class="progress-step <?php echo $step >= 3 ? 'completed' : ''; ?> <?php echo $step == 3 ? 'active' : ''; ?>" data-step="3"></div>
<div class="progress-step <?php echo $step >= 4 ? 'completed' : ''; ?> <?php echo $step == 4 ? 'active' : ''; ?>" data-step="4"></div>
<div class="progress-step <?php echo $step >= 5 ? 'completed' : ''; ?> <?php echo $step == 5 ? 'active' : ''; ?>" data-step="5"></div>
</div>
</div>
<div class="installer-body">
<?php if ($step === 1): ?>
<h2>第一步:环境检测</h2>
<ul class="requirements">
<li class="<?php echo PHP_VERSION_ID >= 70000 ? 'ok' : 'fail'; ?>">
PHP 版本 >= 7.0 (当前: <?php echo PHP_VERSION; ?>)
<?php echo PHP_VERSION_ID >= 70000 ? '✓' : '✗'; ?>
</li>
<li class="<?php echo extension_loaded('mysqli') ? 'ok' : 'fail'; ?>">
MySQLi 扩展 <?php echo extension_loaded('mysqli') ? '✓ 已启用' : '✗ 未启用'; ?>
</li>
<li class="<?php echo is_writable(__DIR__) ? 'ok' : 'fail'; ?>">
目录可写权限 <?php echo is_writable(__DIR__) ? '✓' : '✗'; ?>
</li>
<li class="<?php echo extension_loaded('pdo') ? 'ok' : 'fail'; ?>">
PDO 扩展 <?php echo extension_loaded('pdo') ? '✓ 已启用' : '✗ 未启用'; ?>
</li>
</ul>
<?php
$requirements_ok = PHP_VERSION_ID >= 70000 && extension_loaded('mysqli') && is_writable(__DIR__);
?>
<?php if ($requirements_ok): ?>
<div class="success">环境检测通过,所有要求均已满足</div>
<?php else: ?>
<div class="error">环境检测未通过,请解决上述问题后继续</div>
<?php endif; ?>
<div class="buttons">
<?php if ($requirements_ok): ?>
<a href="?step=2" class="btn">下一步 →</a>
<?php else: ?>
<button class="btn" disabled>下一步 →</button>
<?php endif; ?>
</div>
<?php elseif ($step === 2): ?>
<h2>第二步:数据库配置</h2>
<?php if ($error): ?>
<div class="error"><?php echo e($error); ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label for="db_host">数据库主机</label>
<input type="text" name="db_host" id="db_host" value="<?php echo e($_SESSION['db_host'] ?? 'localhost'); ?>" required>
</div>
<div class="form-group">
<label for="db_port">数据库端口</label>
<input type="number" name="db_port" id="db_port" value="<?php echo e($_SESSION['db_port'] ?? 3306); ?>" required>
</div>
<div class="form-group">
<label for="db_user">数据库用户名</label>
<input type="text" name="db_user" id="db_user" value="<?php echo e($_SESSION['db_user'] ?? 'root'); ?>" required>
</div>
<div class="form-group">
<label for="db_pass">数据库密码</label>
<input type="password" name="db_pass" id="db_pass" value="<?php echo e($_SESSION['db_pass'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="db_name">数据库名称</label>
<input type="text" name="db_name" id="db_name" value="<?php echo e($_SESSION['db_name'] ?? 'tblog'); ?>" required>
</div>
<div class="buttons">
<a href="?step=1" class="btn btn-secondary">← 上一步</a>
<button type="submit" class="btn">测试连接并继续 →</button>
</div>
</form>
<?php elseif ($step === 3): ?>
<h2>第三步:创建配置文件</h2>
<?php if ($error): ?>
<div class="error"><?php echo e($error); ?></div>
<?php endif; ?>
<p>正在创建 <code>config.php</code> 配置文件...</p>
<div class="buttons">
<a href="?step=2" class="btn btn-secondary">← 上一步</a>
<a href="?step=4" class="btn">继续 →</a>
</div>
<?php elseif ($step === 4): ?>
<h2>第四步:安装完成</h2>
<div class="success">🎉 恭喜!TBlog 已安装成功!</div>
<div class="features">
<strong>默认管理员信息:</strong>
<ul>
<li>用户名:admin</li>
<li>密码:admin123</li>
<li style="color:#dc2626;">密码已使用 bcrypt 哈希加密存储</li>
</ul>
</div>
<div class="info-box">
<strong>⚠️ 安全提示:</strong>安装完成后请前往「设置」页面修改默认密码。
</div>
<div class="buttons">
<a href="?step=5" class="btn">完成安装</a>
</div>
<?php elseif ($step === 5): ?>
<div class="text-center">
<h2>🎉 安装完成!</h2>
<p class="mt-4">感谢使用 TBlog</p>
</div>
<div class="buttons" style="justify-content: center; margin-top: 30px;">
<a href="index.php" class="btn">访问博客</a>
<a href="admin/" class="btn btn-secondary">管理后台</a>
</div>
<?php endif; ?>
</div>
</div>
</body>
</html>

58
install.sql Normal file
View file

@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS `categories` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(100) NOT NULL COMMENT '分类名称',
`slug` VARCHAR(100) NOT NULL COMMENT 'URL别名',
`description` TEXT,
`parent_id` INT DEFAULT 0,
`order_num` INT DEFAULT 0,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `posts` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(255) NOT NULL COMMENT '标题',
`content` LONGTEXT NOT NULL COMMENT '正文',
`excerpt` TEXT COMMENT '摘要',
`slug` VARCHAR(200) NOT NULL COMMENT 'URL别名',
`category_id` INT DEFAULT 1,
`template` VARCHAR(50) DEFAULT 'flow',
`status` ENUM('draft','published') DEFAULT 'published',
`views` INT DEFAULT 0,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` DATETIME DEFAULT NULL,
KEY `slug` (`slug`),
KEY `category_id` (`category_id`),
KEY `status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `comments` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`post_id` INT NOT NULL,
`author` VARCHAR(100) NOT NULL COMMENT '评论者',
`email` VARCHAR(200) DEFAULT '',
`url` VARCHAR(500) DEFAULT '',
`content` TEXT NOT NULL,
`ip` VARCHAR(50) DEFAULT '',
`status` ENUM('pending','approved','rejected') DEFAULT 'pending',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY `post_id` (`post_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `settings` (
`key` VARCHAR(100) PRIMARY KEY,
`value` TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default data
INSERT INTO `categories` (`name`, `slug`, `description`, `order_num`) VALUES
('技术', 'tech', '技术文章', 1),
('生活', 'life', '生活随笔', 2),
('摄影', 'photo', '摄影作品', 3);
INSERT INTO `settings` (`key`, `value`) VALUES
('site_name', '张璞博客'),
('site_description', '一个简洁优雅的博客'),
('template', 'flow'),
('posts_per_page', '10');