页脚入口与安装向导;加固管理员口令存储
需求落实: 1. 页脚常驻「管理后台」链接,不用再手敲 admin.php。 2. 页脚「由 FeedLite 驱动」中的 FeedLite 改为超链接(指向开源仓库); 后台新增「页脚」设置组,可关闭整句(关闭后只留管理后台链接),也可改链接地址。 3. 管理员口令存储方式在后台如实展示:数据库 settings.admin.password_hash, 存 bcrypt 单向哈希(不可逆),明文不落盘。 4. 新增安装向导 install.php:data/ 下没有数据库时自动 302 引导; 体检环境 -> 填站点信息与口令 -> 生成 config.php -> 建库建表 -> 完成。 顺带修掉一个真实安全隐患: config.php 里的出厂占位口令 CHANGE_ME_admin 原先可被当作有效口令登录后台 (等于公开仓库里就写着线上后门口令)。现改为占位/空值一律拒绝, 未设口令时后台明确提示去安装向导,不再含糊报「口令不正确」。 实现要点: - 新增 fb_cfg_file():只读配置文件、不叠加 settings 表、绝不触达数据库。 「是否已安装」以数据库文件是否存在为判据,而 fb_cfg() 读 settings 会触发 fb_db() 就地建库 —— 那会把安装态自己造出来,所以判定必须走不碰库的通道。 - bootstrap 只在已安装时建库;未安装则 302 到 install.php(CLI 下报错退出)。 - fb_settings_flat() 在未安装时直接返回空,杜绝任何隐式建库。 - install.php 用文本级替换生成 config.php,保留全部注释与 OSS/SMTP 段落; 生成的 PHP 用 token_get_all(TOKEN_PARSE) 校验后才落盘。 - 页脚链接只放行 http/https,后台保存时校验(防 javascript: 伪协议)。 - style.css 页脚行改 flex,版本号 v6 -> v7。 验证: - php -l 全量通过;现有回归 25/25、附件专项 13/13 保持通过。 - 安装向导专项 60/60(含「明文口令未写入任何文件」「数据库字节里搜不到明文」)。 - 反向验证:撤销三处修复后对应断言确实失败(断言非装饰)。 - 几何检查:本机 15/15 + 页脚专项全过;线上真实 URL 15/15 + 页脚专项全过。 - 线上真机冒烟 22/22。
This commit is contained in:
@@ -25,6 +25,7 @@ feedback/
|
|||||||
├─ thread.php 问题详情 + 回复(含"官方答复"与状态管理、附件展示)
|
├─ thread.php 问题详情 + 回复(含"官方答复"与状态管理、附件展示)
|
||||||
├─ new.php 发布新问题(含图片 / 视频上传)
|
├─ new.php 发布新问题(含图片 / 视频上传)
|
||||||
├─ admin.php 管理后台(口令登录:审核 / 管理全部问题 / 站点设置 / 操作日志)
|
├─ admin.php 管理后台(口令登录:审核 / 管理全部问题 / 站点设置 / 操作日志)
|
||||||
|
├─ install.php 安装向导(数据库不存在时自动引导进来;装完建议删除)
|
||||||
├─ upload.php 附件上传接口(两步:领对象键 → 收文件本体,local 落盘 / OSS 代理 PUT)
|
├─ upload.php 附件上传接口(两步:领对象键 → 收文件本体,local 落盘 / OSS 代理 PUT)
|
||||||
├─ media.php 附件代理输出(私有桶 + 内网 Endpoint 下图片/视频的唯一对外读取通道,带鉴权与 Range)
|
├─ media.php 附件代理输出(私有桶 + 内网 Endpoint 下图片/视频的唯一对外读取通道,带鉴权与 Range)
|
||||||
├─ captcha.php "换一题"用的人机验证刷新接口(返回 JSON)
|
├─ captcha.php "换一题"用的人机验证刷新接口(返回 JSON)
|
||||||
@@ -60,8 +61,9 @@ feedback/
|
|||||||
chown -R www:www /www/wwwroot/你的域名/feedback
|
chown -R www:www /www/wwwroot/你的域名/feedback
|
||||||
chmod -R 755 /www/wwwroot/你的域名/feedback/data
|
chmod -R 755 /www/wwwroot/你的域名/feedback/data
|
||||||
```
|
```
|
||||||
4. 首次访问 `https://你的域名/feedback/` 会自动建库建表。
|
4. 首次访问 `https://你的域名/feedback/` —— 此时 `data/` 下还没有数据库,
|
||||||
5. **打开 `config.php`,修改这三项**(务必!):
|
程序会**自动把你送到安装向导 `install.php`**(见 **第二节之二**)。按向导填完即装好。
|
||||||
|
5. 若你更愿意手工配置:从 `config.example.php` 复制一份为 `config.php`,改掉这三项后刷新页面即可建库。
|
||||||
- `admin_password` —— 管理员口令(回复时填入即以「官方」身份答复;也是审核后台 `admin.php` 的登录口令)
|
- `admin_password` —— 管理员口令(回复时填入即以「官方」身份答复;也是审核后台 `admin.php` 的登录口令)
|
||||||
- `api_token` —— 读取接口的令牌(自己设一串随机字符)
|
- `api_token` —— 读取接口的令牌(自己设一串随机字符)
|
||||||
- `ip_salt` —— 随机字符串(用于给访客 IP 做哈希)
|
- `ip_salt` —— 随机字符串(用于给访客 IP 做哈希)
|
||||||
@@ -76,11 +78,50 @@ feedback/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 二之二、安装向导(`install.php`)
|
||||||
|
|
||||||
|
**什么时候出现**:只要 `data/` 下还没有数据库文件,访问站点的任何页面都会 302 到 `install.php`。
|
||||||
|
反过来,数据库一旦存在,向导就只显示「已经装好了」,**不会**覆盖任何数据。
|
||||||
|
|
||||||
|
向导分三步,一屏走完:
|
||||||
|
|
||||||
|
1. **环境体检** —— PHP 版本(≥7.4)、必需扩展(`pdo_sqlite` / `openssl`)、
|
||||||
|
`站点根 / data / uploads` 目录可写性、`config.example.php` 是否在。有任一项不过就拒绝提交。
|
||||||
|
2. **填写站点信息** —— 站点标题、站点描述、官方邮箱、站点根地址(后两项可留空)。
|
||||||
|
3. **设置管理员口令** —— 至少 8 位;也可以留空、装完后在后台补设(**补设之前后台登不进去**)。
|
||||||
|
|
||||||
|
提交后向导会:
|
||||||
|
|
||||||
|
- 生成 `config.php`(在 `config.example.php` 的**文本**上逐项替换,注释与 OSS / SMTP 段落原样保留);
|
||||||
|
- 自动填好 `api_token` 与 `ip_salt`(随机串,不用你操心);
|
||||||
|
- 建库建表;
|
||||||
|
- 把口令以 **bcrypt 哈希**写入数据库 `settings` 表 —— **明文不进 `config.php`、不进任何文件**。
|
||||||
|
|
||||||
|
**装完请做两件事**:
|
||||||
|
|
||||||
|
1. **删掉 `install.php`**(已安装态下它会拒绝重装,但少一个入口少一分风险)。
|
||||||
|
2. 若口令留空了,进后台「站点设置 → 登录口令」补设一个。
|
||||||
|
|
||||||
|
> 想重装(例如换了数据库位置):先手工移走 `data/feedback.db`,再刷新即可。
|
||||||
|
> 向导**不会**替你删除任何数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 三、首页加链接
|
## 三、首页加链接
|
||||||
|
|
||||||
已在官网 `index.html` 的**顶部导航**与**页脚「产品」列**加入「问题反馈」入口,指向 `feedback/`。
|
已在官网 `index.html` 的**顶部导航**与**页脚「产品」列**加入「问题反馈」入口,指向 `feedback/`。
|
||||||
(本地文件:`(你的站点首页)`)
|
(本地文件:`(你的站点首页)`)
|
||||||
|
|
||||||
|
### 页脚里有「管理后台」入口
|
||||||
|
|
||||||
|
站点页脚固定显示一个「管理后台」链接(指向 `admin.php`),不需要手动在地址栏敲文件名。
|
||||||
|
旁边那句「由 FeedLite 驱动」可以关掉:
|
||||||
|
|
||||||
|
- 后台「**站点设置 → 页脚**」→ 取消勾选「在页脚显示『由 FeedLite 驱动』」
|
||||||
|
→ 页脚只剩「管理后台」链接;
|
||||||
|
- 同一处还能改「FeedLite」二字指向的地址(默认是本项目开源仓库)。
|
||||||
|
只接受 `http://` / `https://` 开头的网址。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、邮件通知(SMTP)
|
## 四、邮件通知(SMTP)
|
||||||
@@ -566,23 +607,42 @@ curl "https://你的域名/feedback/api.php?token=XXX&action=digest&status=open"
|
|||||||
- `data/` 目录用 `.htaccess` 拒绝一切直接访问;请勿删除该文件。
|
- `data/` 目录用 `.htaccess` 拒绝一切直接访问;请勿删除该文件。
|
||||||
|
|
||||||
### 更换管理员口令 / 令牌
|
### 更换管理员口令 / 令牌
|
||||||
直接修改 `config.php` 即可,无需改代码。
|
|
||||||
|
- **管理员口令**:登录后台后在「站点设置 → 登录口令」里改(至少 8 位)。改完即以 bcrypt 哈希存进数据库,`config.php` 里的 `admin_password` 只是「未设置」时的初始值通道。
|
||||||
|
- **令牌 `api_token` / 盐 `ip_salt`**:这两项没有后台界面,改 `config.php` 即可,无需改代码。
|
||||||
|
|
||||||
|
### 口令存在哪、是否加密(重要)
|
||||||
|
|
||||||
|
| 状态 | 存在哪 | 是否加密 | 怎么变成下一档 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 刚 clone 下来、没装 | `config.php` 的 `admin_password` = `CHANGE_ME_admin` | 明文,但**该值已被程序明确禁用**,无法登录 | 跑 `install.php`,或到后台设定口令 |
|
||||||
|
| 手工在 `config.php` 指定了口令 | `config.php` 的 `admin_password` | **明文**(配置文件里) | 登录一次后到后台改一次口令 → 转为数据库哈希 |
|
||||||
|
| 正常状态(推荐) | 数据库 `settings` 表的 `admin.password_hash` | **是**,bcrypt 单向哈希(`$2y$…`),不可逆 | 已是最终形态 |
|
||||||
|
|
||||||
|
- 后台「站点设置 → 登录口令」会把当前处于哪一档**如实显示出来**,Linux 下也能直接查库确认。
|
||||||
|
- 明文口令**永远不会**由安装向导或后台写进任何文件:向导只把哈希写进数据库。
|
||||||
|
- 怀疑口令泄露:到后台改掉即可。若把数据库里 `admin.password_hash` 这一行删掉,
|
||||||
|
会退回使用 `config.php` 的值(也因此回到明文档,建议删完立刻重新设定)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 七之二、`config.php` 里的三个"必改"参数是什么
|
## 七之二、`config.php` 里的三个"必改"参数是什么
|
||||||
|
|
||||||
这三项都写在 `config.php` 的「安全」段落里,出厂值都是 `CHANGE_ME_...` 占位符,**上线前必须换成自己的随机值**。
|
> **走安装向导的话,这一段可以跳过**:`install.php` 会替你生成 `config.php`,
|
||||||
|
> 并自动填好 `api_token` 与 `ip_salt`(都是随机串),管理员口令则直接以哈希入库、不进文件。
|
||||||
|
> 下面这些是「想手工配置」时看的说明。三项出厂值都是 `CHANGE_ME_...` 占位符。
|
||||||
|
|
||||||
### `admin_password` —— 登录口令(初始值)
|
### `admin_password` —— 登录口令(初始值,可留空)
|
||||||
|
|
||||||
- **干什么用**:两个地方共用同一串,所以必须一致 ——
|
- **干什么用**:两个地方共用同一串 ——
|
||||||
① 详情页回复框里填它 → 以「官方」身份发言;
|
① 详情页回复框里填它 → 以「官方」身份发言;
|
||||||
② `/feedback/admin.php` 登录 → 进管理后台(审核、回复、改状态、删帖、改站点设置)。
|
② `/feedback/admin.php` 登录 → 进管理后台(审核、回复、改状态、删帖、改站点设置)。
|
||||||
- **建议**:**登录后台后在「站点设置 → 修改登录口令」里改成你自己的一串**(至少 8 位)。
|
- **建议**:优先用安装向导;若手工配,**登录后台后立刻在「站点设置 → 登录口令」里改成你自己的一串**(至少 8 位)。
|
||||||
改完会以 `password_hash()` 存进数据库,`config.php` 里这行就只是"出厂默认值"了 ——
|
改完会以 `password_hash()` 存进数据库,`config.php` 里这行就只是"出厂默认值"了 ——
|
||||||
⚠️ 此时改 `config.php` **不会**再影响登录口令,忘了新口令就去数据库把 `settings` 表里
|
⚠️ 此时改 `config.php` **不会**再影响登录口令,忘了新口令就去数据库把 `settings` 表里
|
||||||
`admin.password_hash` 这一行删掉,即可退回用 `config.php` 里的值。
|
`admin.password_hash` 这一行删掉,即可退回用 `config.php` 里的值。
|
||||||
|
- ⚠️ **占位值 `CHANGE_ME_admin` 已被程序明确禁用**(空值同样):也就是说,**clone 下来不做任何配置时,
|
||||||
|
后台任何人都登不进去** —— 这是刻意的,避免"文档里公开的默认口令"变成线上后门。
|
||||||
|
|
||||||
- **它不是什么**:站点面向访客没有账号体系,也没有访客登录页。它只是一串比对用的口令 ——
|
- **它不是什么**:站点面向访客没有账号体系,也没有访客登录页。它只是一串比对用的口令 ——
|
||||||
表单里填的内容与库里的哈希(或 `config.php` 里的值)比对一次,一致就按官方处理。
|
表单里填的内容与库里的哈希(或 `config.php` 里的值)比对一次,一致就按官方处理。
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
|||||||
$loginErr = '会话已过期,请重试。';
|
$loginErr = '会话已过期,请重试。';
|
||||||
} elseif (!fb_rate_check('admin_login', 10)) {
|
} elseif (!fb_rate_check('admin_login', 10)) {
|
||||||
$loginErr = '尝试次数过多,请一小时后再试。';
|
$loginErr = '尝试次数过多,请一小时后再试。';
|
||||||
|
} elseif (!fb_admin_password_is_set()) {
|
||||||
|
$loginErr = '本系统还没有设置登录口令,请先运行安装向导(install.php)完成设置。';
|
||||||
} elseif ($pw !== '' && fb_admin_verify_password($pw)) {
|
} elseif ($pw !== '' && fb_admin_verify_password($pw)) {
|
||||||
fb_admin_login();
|
fb_admin_login();
|
||||||
fb_mod_log('login');
|
fb_mod_log('login');
|
||||||
@@ -46,7 +48,10 @@ if (!fb_admin_logged_in()) {
|
|||||||
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
|
<div class="crumbs"><a href="index.php">← 返回问题列表</a></div>
|
||||||
<section class="narrow">
|
<section class="narrow">
|
||||||
<h1>管理登录</h1>
|
<h1>管理登录</h1>
|
||||||
<p class="muted">请输入管理员口令(初始口令在 <code>config.php</code> 的 <code>admin_password</code>,登录后在「站点设置」里可以改掉它)。</p>
|
<p class="muted">请输入管理员口令<?php if (fb_admin_password_source() === 'file'): ?>(当前仍是 <code>config.php</code> 里的 <code>admin_password</code> 明文,登录后请到「站点设置」改成加密存储)<?php endif; ?>。</p>
|
||||||
|
<?php if (fb_admin_password_source() === 'none'): ?>
|
||||||
|
<div class="flash error">尚未设置登录口令,且出厂占位口令已被禁用。请运行 <a href="install.php">install.php</a> 设定一个。</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php fb_flash_box($loginErr, 'error'); ?>
|
<?php fb_flash_box($loginErr, 'error'); ?>
|
||||||
<form method="post" action="admin.php" class="card-form">
|
<form method="post" action="admin.php" class="card-form">
|
||||||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||||
@@ -184,7 +189,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
|||||||
$old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : '');
|
$old = (string)(isset($_POST['old_password']) ? $_POST['old_password'] : '');
|
||||||
$n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : '');
|
$n1 = (string)(isset($_POST['new_password']) ? $_POST['new_password'] : '');
|
||||||
$n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : '');
|
$n2 = (string)(isset($_POST['new_password2']) ? $_POST['new_password2'] : '');
|
||||||
if (!fb_admin_verify_password($old)) {
|
// 从没设过口令(或仍是出厂占位值)时,跳过"当前口令"校验 —— 否则谁也进不来、又改不了
|
||||||
|
$firstSet = !fb_admin_password_is_set();
|
||||||
|
if (!$firstSet && !fb_admin_verify_password($old)) {
|
||||||
$msg = '当前口令不正确。';
|
$msg = '当前口令不正确。';
|
||||||
$msgType = 'error';
|
$msgType = 'error';
|
||||||
} elseif (fb_len($n1) < 8) {
|
} elseif (fb_len($n1) < 8) {
|
||||||
@@ -195,8 +202,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['act'])) {
|
|||||||
$msgType = 'error';
|
$msgType = 'error';
|
||||||
} else {
|
} else {
|
||||||
fb_admin_set_password($n1);
|
fb_admin_set_password($n1);
|
||||||
fb_mod_log('password');
|
fb_mod_log('password', null, $firstSet ? '首次设定登录口令(bcrypt 哈希存库)' : '修改登录口令');
|
||||||
$msg = '口令已修改,下次登录请使用新口令。';
|
$msg = $firstSet ? '口令已设定,下次登录请使用它。' : '口令已修改,下次登录请使用新口令。';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +230,16 @@ function fb_admin_save_settings() {
|
|||||||
else { fb_set_setting($k, $v); }
|
else { fb_set_setting($k, $v); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页脚:是否显示「由 FeedLite 驱动」这句版权,以及链接指向的开源仓库地址
|
||||||
|
fb_set_setting('footer.show_credit', $post('footer_show_credit') ? 1 : 0);
|
||||||
|
$curl = trim((string)$post('footer_credit_url', ''));
|
||||||
|
// 只接受 http/https:这条会被直接渲染成 <a href>,不能让 javascript:/data: 混进来
|
||||||
|
if ($curl !== '' && !preg_match('~^https?://~i', $curl)) {
|
||||||
|
return '页脚链接地址需以 http:// 或 https:// 开头。';
|
||||||
|
}
|
||||||
|
if ($curl === '' || $curl === fb_credit_url_default()) { fb_unset_setting('footer.credit_url'); }
|
||||||
|
else { fb_set_setting('footer.credit_url', $curl); }
|
||||||
|
|
||||||
$ints = [
|
$ints = [
|
||||||
'upload.image_max_per_post' => [0, 20],
|
'upload.image_max_per_post' => [0, 20],
|
||||||
'upload.video_max_per_post' => [0, 10],
|
'upload.video_max_per_post' => [0, 10],
|
||||||
@@ -544,6 +561,14 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
placeholder="欢迎在这里反馈使用问题、提出功能建议。"><?php echo e((string)fb_cfg('site_desc', '')); ?></textarea>
|
placeholder="欢迎在这里反馈使用问题、提出功能建议。"><?php echo e((string)fb_cfg('site_desc', '')); ?></textarea>
|
||||||
<i class="hint">显示在首页标题下方,一句话说明这里可以反馈什么。两项都留空则恢复为出厂默认。</i></label>
|
<i class="hint">显示在首页标题下方,一句话说明这里可以反馈什么。两项都留空则恢复为出厂默认。</i></label>
|
||||||
|
|
||||||
|
<h2 style="margin:24px 0 10px">页脚</h2>
|
||||||
|
<label class="check"><input type="checkbox" name="footer_show_credit" value="1"<?php echo fb_cfg('footer.show_credit', true) ? ' checked' : ''; ?>> 在页脚显示「由 FeedLite 驱动」
|
||||||
|
<span class="hint" style="display:block;margin:2px 0 0 24px">取消勾选后,这句话连同链接都不再出现在页脚,只保留「管理后台」入口。</span></label>
|
||||||
|
<label class="field"><span>FeedLite 链接地址</span>
|
||||||
|
<input type="text" name="footer_credit_url" maxlength="300"
|
||||||
|
value="<?php echo e((string)fb_cfg('footer.credit_url', '') !== '' ? (string)fb_cfg('footer.credit_url') : fb_credit_url_default()); ?>">
|
||||||
|
<i class="hint">页脚「FeedLite」二字指向的地址,默认是本项目开源仓库。只接受 http:// 或 https:// 开头的网址;填回默认值即恢复。</i></label>
|
||||||
|
|
||||||
<h2 style="margin:24px 0 10px">附件存储</h2>
|
<h2 style="margin:24px 0 10px">附件存储</h2>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>附件保存在哪</span>
|
<span>附件保存在哪</span>
|
||||||
@@ -649,19 +674,57 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 style="margin:26px 0 10px">修改登录口令</h2>
|
<h2 style="margin:26px 0 10px">登录口令</h2>
|
||||||
|
<div class="card-form" style="margin:0 0 16px">
|
||||||
|
<?php $pwSrc = fb_admin_password_source(); ?>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px">
|
||||||
|
<span>存储位置:
|
||||||
|
<?php if ($pwSrc === 'db'): ?>
|
||||||
|
数据库 <code>settings</code> 表(键名 <code>admin.password_hash</code>)
|
||||||
|
<?php elseif ($pwSrc === 'file'): ?>
|
||||||
|
<code>config.php</code> 的 <code>admin_password</code>
|
||||||
|
<?php else: ?>
|
||||||
|
<b style="color:#b45309">尚未设置</b>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span>是否加密:
|
||||||
|
<?php if ($pwSrc === 'db'): ?>
|
||||||
|
<b>是</b> —— 存的是 <code>password_hash()</code> 生成的 bcrypt 单向哈希(<code>$2y$…</code>),
|
||||||
|
不可逆、无法还原明文;校验用 <code>password_verify()</code>。
|
||||||
|
<?php elseif ($pwSrc === 'file'): ?>
|
||||||
|
<b>否</b> —— 目前是明文写在 <code>config.php</code> 里。
|
||||||
|
<a href="#pwform">在下面改一次口令</a>后即会转存为数据库中的 bcrypt 哈希。
|
||||||
|
<?php else: ?>
|
||||||
|
—— 还没有可用口令,当前后台<strong>无法登录</strong>。请用下方表单设定一个。
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<?php if ($pwSrc === 'db'): ?>
|
||||||
|
<span class="muted small">数据库文件在 <code>data/feedback.db</code>(已由 .htaccess 拒绝外部访问),且只存哈希,明文不落盘。</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pwform">
|
||||||
<form method="post" action="admin.php?tab=settings" class="card-form">
|
<form method="post" action="admin.php?tab=settings" class="card-form">
|
||||||
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||||
|
<?php if ($pwSrc !== 'none'): ?>
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
<label class="field"><span>当前口令</span><input type="password" name="old_password" autocomplete="current-password"></label>
|
<label class="field"><span>当前口令</span><input type="password" name="old_password" autocomplete="current-password"></label>
|
||||||
<label class="field"><span>新口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
|
<label class="field"><span>新口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
|
||||||
<label class="field"><span>再输入一次新口令</span><input type="password" name="new_password2" autocomplete="new-password"></label>
|
<label class="field"><span>再输入一次新口令</span><input type="password" name="new_password2" autocomplete="new-password"></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<?php else: ?>
|
||||||
<button class="btn" type="submit" name="act" value="password">修改口令</button>
|
<div class="grid2">
|
||||||
|
<label class="field"><span>设定登录口令(至少 8 位)</span><input type="password" name="new_password" autocomplete="new-password"></label>
|
||||||
|
<label class="field"><span>再输入一次</span><input type="password" name="new_password2" autocomplete="new-password"></label>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted small">新口令保存在数据库里,优先级高于 <code>config.php</code> 的 <code>admin_password</code>;改完立即生效。</p>
|
<?php endif; ?>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn primary" type="submit" name="act" value="password"><?php echo $pwSrc === 'none' ? '设定口令' : '修改口令'; ?></button>
|
||||||
|
</div>
|
||||||
|
<p class="muted small">新口令以 bcrypt 哈希写入数据库,优先级高于 <code>config.php</code> 的 <code>admin_password</code>;改完立即生效,明文不会留在任何文件里。</p>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh}
|
|||||||
.foot{border-top:1px solid var(--line); background:var(--card); padding:22px 0 34px; margin-top:40px}
|
.foot{border-top:1px solid var(--line); background:var(--card); padding:22px 0 34px; margin-top:40px}
|
||||||
.foot p{margin:4px 0; text-align:center; font-size:13px; color:var(--muted)}
|
.foot p{margin:4px 0; text-align:center; font-size:13px; color:var(--muted)}
|
||||||
.foot a{color:var(--muted); text-decoration:underline}
|
.foot a{color:var(--muted); text-decoration:underline}
|
||||||
|
/* 版权句 + 管理后台入口:同一行居中,窄屏自动换行 */
|
||||||
|
.foot-line{display:flex; flex-wrap:wrap; align-items:center; justify-content:center; gap:0 6px}
|
||||||
|
.foot-line .dot{padding:0}
|
||||||
|
|
||||||
/* ---------- 按钮 ---------- */
|
/* ---------- 按钮 ---------- */
|
||||||
.btn{
|
.btn{
|
||||||
|
|||||||
+19
-3
@@ -23,9 +23,25 @@ return [
|
|||||||
// 例如:'https://feedback.example.com'
|
// 例如:'https://feedback.example.com'
|
||||||
'base_url' => '',
|
'base_url' => '',
|
||||||
|
|
||||||
/* ---------------- 安全 ---------------- */
|
/* ---------------- 页脚 ----------------
|
||||||
// 管理员口令:在"回复"表单里填入它,即以「官方」身份回复(高亮显示),并触发邮件通知。
|
* 页脚永远会显示「管理后台」入口;这里的开关只控制右侧那句版权说明。
|
||||||
// ★★ 请务必修改! ★★
|
* show_credit = false:页脚不再出现「由 FeedLite 驱动」,只留管理后台链接。
|
||||||
|
*/
|
||||||
|
'footer' => [
|
||||||
|
'show_credit' => true,
|
||||||
|
'credit_url' => 'https://code.yangyq.net/laoyang/FeedLite', // 页脚「FeedLite」二字指向的地址
|
||||||
|
],
|
||||||
|
|
||||||
|
/* ---------------- 安全 ----------------
|
||||||
|
* 管理员口令的存放规则(重要):
|
||||||
|
* · 正常安装(走 install.php,或登录后台改口令)→ 口令以 **bcrypt 单向哈希**
|
||||||
|
* 存在数据库 settings 表的 admin.password_hash,**明文不落盘**。
|
||||||
|
* · 下面这个 admin_password 只是「初始值」通道,且**出厂占位值已被程序明确禁用**:
|
||||||
|
* 值仍为 CHANGE_ME_admin 时,任何人(包括你)都无法用它登录后台。
|
||||||
|
* · 想手工指定初始口令,就把下面这行改成你自己的口令;登录一次后建议立即改掉,
|
||||||
|
* 改完即转为数据库哈希存储、这里可以再改回占位值。
|
||||||
|
*/
|
||||||
|
// ★★ 留空或保持占位值 = 未设置口令(后台不可登录,需先跑安装向导)★★
|
||||||
'admin_password' => 'CHANGE_ME_admin',
|
'admin_password' => 'CHANGE_ME_admin',
|
||||||
|
|
||||||
// API 令牌:供自动化脚本 / AI 读取数据用。★ 请务必修改 ★
|
// API 令牌:供自动化脚本 / AI 读取数据用。★ 请务必修改 ★
|
||||||
|
|||||||
+371
@@ -0,0 +1,371 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* FeedLite · 安装向导
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* 什么时候会用到:`data/` 下还没有数据库文件(全新部署)时,任何页面都会把
|
||||||
|
* 访问者送到这里。向导只做四件事:
|
||||||
|
* ① 体检:PHP 版本、必需扩展、目录可写性;
|
||||||
|
* ② 收信息:站点标题/描述、管理员口令、官方邮箱、站点根地址;
|
||||||
|
* ③ 写配置:生成 config.php(在 config.example.php 基础上逐项替换,
|
||||||
|
* 已有的 OSS / SMTP 段落原样保留,不覆盖你手工改过的内容);
|
||||||
|
* ④ 建库建表 + 把口令以 bcrypt 哈希写进数据库 ——
|
||||||
|
* **明文口令不落盘**,config.php 里始终保持出厂占位值。
|
||||||
|
*
|
||||||
|
* 装完之后:删除本文件与否都可以(已安装态下它会礼貌地拒绝重复安装),
|
||||||
|
* 但建议删掉,少一个入口少一分风险。
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* 安装向导必须能在「尚未安装」的状态下运行,所以这里自己引导,
|
||||||
|
* 并显式声明跳过 bootstrap 的「未安装就跳 install」逻辑,否则会自己跳自己。
|
||||||
|
*/
|
||||||
|
define('FB_SKIP_INSTALL_REDIRECT', 1);
|
||||||
|
require __DIR__ . '/lib/bootstrap.php';
|
||||||
|
|
||||||
|
$root = FB_ROOT;
|
||||||
|
$cfgFile = $root . '/config.php';
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 已安装 → 拒绝重复安装
|
||||||
|
* ================================================================== */
|
||||||
|
$already = fb_installed();
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 环境体检
|
||||||
|
* ================================================================== */
|
||||||
|
function fb_install_checks() {
|
||||||
|
$need = ['pdo_sqlite' => 'SQLite 数据库(PDO)', 'openssl' => 'SMTP / OSS 加密通信'];
|
||||||
|
$opt = ['curl' => '访问 OSS(可选,没有会退回 PHP 流式请求)', 'mbstring' => '中文字数统计(可选)'];
|
||||||
|
|
||||||
|
$dirs = [
|
||||||
|
FB_ROOT => '站点根目录(要写入 config.php)',
|
||||||
|
FB_ROOT . '/data' => '数据库目录 data/',
|
||||||
|
FB_ROOT . '/uploads' => '本机附件目录 uploads/(本机存储模式用)',
|
||||||
|
];
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$ok = true;
|
||||||
|
|
||||||
|
$verOk = version_compare(PHP_VERSION, '7.4.0', '>=');
|
||||||
|
$rows[] = ['PHP 版本', PHP_VERSION, $verOk, '需要 7.4 及以上(推荐 8.0~8.3)', $verOk ? '' : '当前版本过低,请切换站点的 PHP 版本'];
|
||||||
|
if (!$verOk) { $ok = false; }
|
||||||
|
|
||||||
|
foreach ($need as $ext => $why) {
|
||||||
|
$has = extension_loaded($ext);
|
||||||
|
if (!$has) { $ok = false; }
|
||||||
|
$rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : '未启用', $has, $why, $has ? '' : ('请在 PHP 设置里开启 ' . $ext . ' 扩展')];
|
||||||
|
}
|
||||||
|
foreach ($opt as $ext => $why) {
|
||||||
|
$has = extension_loaded($ext);
|
||||||
|
$rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : '未启用(不影响安装)', true, $why, ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dirs as $d => $why) {
|
||||||
|
if (!is_dir($d)) { @mkdir($d, 0755, true); }
|
||||||
|
$w = is_dir($d) && is_writable($d);
|
||||||
|
if (!$w) { $ok = false; }
|
||||||
|
$shown = ($d === FB_ROOT) ? '(站点根)' : str_replace(FB_ROOT, '', $d);
|
||||||
|
$rows[] = ['目录可写 ' . $shown, $w ? '可写' : '不可写', $w, $why, $w ? '' : ('请给 Web 用户写权限,例如 chown www:www ' . $d)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// config.php 若已存在(例如从模板复制过),必须可写才能就地更新
|
||||||
|
if (is_file(FB_ROOT . '/config.php')) {
|
||||||
|
$w = is_writable(FB_ROOT . '/config.php');
|
||||||
|
if (!$w) { $ok = false; }
|
||||||
|
$rows[] = ['config.php 可写', $w ? '可写' : '不可写', $w, '需要就地更新配置', $w ? '' : '请给该文件写权限,例如 chown www:www config.php'];
|
||||||
|
} else {
|
||||||
|
$rows[] = ['config.php', '尚不存在(将自动生成)', true, '由安装向导创建', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ex = is_file(FB_ROOT . '/config.example.php');
|
||||||
|
$rows[] = ['配置模板 config.example.php', $ex ? '存在' : '缺失', $ex, '生成 config.php 的底稿', $ex ? '' : '仓库文件不完整,请重新获取源码'];
|
||||||
|
if (!$ex) { $ok = false; }
|
||||||
|
|
||||||
|
return [$ok, $rows];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 生成 config.php(在模板文本上逐项替换,保留注释与其它段落)
|
||||||
|
* ================================================================== */
|
||||||
|
function fb_install_write_config(array $v) {
|
||||||
|
$example = FB_ROOT . '/config.example.php';
|
||||||
|
$src = (string)@file_get_contents($example);
|
||||||
|
if ($src === '') { return '读不到 config.example.php。'; }
|
||||||
|
|
||||||
|
/** 把顶层键(含引号形式)整项替换成新值;找不到就返回原串由调用方兜底 */
|
||||||
|
$setTop = function ($key, $valueExpr, $txt) {
|
||||||
|
$pat = "/^([ \t]*'" . preg_quote($key, '/') . "'[ \t]*=>[ \t]*)(.*?)(,[ \t]*)(\r?\n)/m";
|
||||||
|
$rep = '$1' . $valueExpr . '$3$4';
|
||||||
|
return preg_replace($pat, $rep, $txt, 1);
|
||||||
|
};
|
||||||
|
$phpStr = function ($s) { return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], (string)$s) . "'"; };
|
||||||
|
|
||||||
|
$dbPath = str_replace('\\', '/', FB_ROOT . '/data/feedback.db');
|
||||||
|
|
||||||
|
$src = $setTop('db_path', "'" . $dbPath . "'", $src);
|
||||||
|
$src = $setTop('site_name', $phpStr($v['site_name']), $src);
|
||||||
|
$src = $setTop('site_desc', $phpStr($v['site_desc']), $src);
|
||||||
|
$src = $setTop('admin_email', $phpStr($v['admin_email']), $src);
|
||||||
|
$src = $setTop('base_url', $phpStr($v['base_url']), $src);
|
||||||
|
// 口令不写进文件:此处只把残留的旧明文重置为占位值(正常都是占位值,等价于不动)
|
||||||
|
$src = $setTop('admin_password', "'CHANGE_ME_admin'", $src);
|
||||||
|
$src = $setTop('api_token', $phpStr($v['api_token']), $src);
|
||||||
|
$src = $setTop('ip_salt', $phpStr($v['ip_salt']), $src);
|
||||||
|
|
||||||
|
// 兜底校验:生成的文本必须是可解析的 PHP。
|
||||||
|
// 注意必须带 TOKEN_PARSE —— 不加这一位 token_get_all 只做词法切分、不报语法错,等于没校验。
|
||||||
|
$syntaxOk = true;
|
||||||
|
if (defined('TOKEN_PARSE') && function_exists('token_get_all')) {
|
||||||
|
try { @token_get_all($src, TOKEN_PARSE); }
|
||||||
|
catch (Throwable $e) { $syntaxOk = false; }
|
||||||
|
}
|
||||||
|
if (!$syntaxOk) { return '生成的配置内容语法不正确,已放弃写入(请把 config.php 恢复为模板后再试)。'; }
|
||||||
|
|
||||||
|
$stamp = date('Y-m-d H:i:s');
|
||||||
|
$header = "<?php\n"
|
||||||
|
. "/**\n"
|
||||||
|
. " * FeedLite · 轻反馈 · 配置文件\n"
|
||||||
|
. " * ------------------------------------------------------------------\n"
|
||||||
|
. " * 本文件由安装向导于 " . $stamp . " 生成,已按你填写的内容更新;\n"
|
||||||
|
. " * 段落注释与 OSS / SMTP 等配置保持模板原样,可继续手工编辑。\n"
|
||||||
|
. " * 注意:管理员口令【不在这里】——它以 bcrypt 哈希存在数据库 settings 表,\n"
|
||||||
|
. " * 下行的 admin_password 只是工厂占位值(已被程序明确禁用,无法登录)。\n"
|
||||||
|
. " */\n\n";
|
||||||
|
$src = $header . preg_replace('/^<\?php\s*/', '', $src, 1);
|
||||||
|
|
||||||
|
$ok = @file_put_contents(FB_ROOT . '/config.php', $src);
|
||||||
|
if ($ok === false) { return '写入 config.php 失败(目录或文件不可写)。'; }
|
||||||
|
@chmod(FB_ROOT . '/config.php', 0640);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 随机串(口令 / 令牌 / 盐) */
|
||||||
|
function fb_install_rand($n = 32) {
|
||||||
|
try { return bin2hex(random_bytes($n)); } catch (Throwable $e) { return md5(uniqid('', true) . mt_rand()); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* 提交处理
|
||||||
|
* ================================================================== */
|
||||||
|
$err = '';
|
||||||
|
$done = false;
|
||||||
|
|
||||||
|
if (isset($_POST['do_install'])) {
|
||||||
|
list($envOk, ) = fb_install_checks();
|
||||||
|
|
||||||
|
$post = function ($k, $d = '') { return isset($_POST[$k]) ? trim((string)$_POST[$k]) : $d; };
|
||||||
|
|
||||||
|
$siteName = $post('site_name');
|
||||||
|
$siteDesc = $post('site_desc');
|
||||||
|
$admEmail = $post('admin_email');
|
||||||
|
$baseUrl = rtrim($post('base_url'), '/');
|
||||||
|
$pw = (string)(isset($_POST['admin_password']) ? $_POST['admin_password'] : '');
|
||||||
|
$pw2 = (string)(isset($_POST['admin_password2']) ? $_POST['admin_password2'] : '');
|
||||||
|
$setPw = !empty($_POST['set_password']);
|
||||||
|
|
||||||
|
if (!$envOk) {
|
||||||
|
$err = '环境体检未通过,请先按提示处理好上面标红的问题。';
|
||||||
|
} elseif (!fb_csrf_ok(isset($_POST['csrf']) ? (string)$_POST['csrf'] : '')) {
|
||||||
|
$err = '会话已过期,请重新提交。';
|
||||||
|
} elseif ($siteName === '' || fb_len($siteName) > 80) {
|
||||||
|
$err = '站点标题请填写 1~80 个字符。';
|
||||||
|
} elseif ($admEmail !== '' && !filter_var($admEmail, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$err = '官方邮箱格式不正确(也可以留空)。';
|
||||||
|
} elseif ($baseUrl !== '' && !preg_match('~^https?://~i', $baseUrl)) {
|
||||||
|
$err = '站点根地址需要以 http:// 或 https:// 开头(留空则自动探测)。';
|
||||||
|
} elseif ($setPw && fb_len($pw) < 8) {
|
||||||
|
$err = '管理员口令至少 8 个字符。';
|
||||||
|
} elseif ($setPw && $pw !== $pw2) {
|
||||||
|
$err = '两次输入的口令不一致。';
|
||||||
|
} else {
|
||||||
|
// ① 先建库建表 —— 后面写设置才有地方写
|
||||||
|
$pdo = fb_db();
|
||||||
|
|
||||||
|
// ② 写 config.php(口令不入文件)
|
||||||
|
$cfgErr = fb_install_write_config([
|
||||||
|
'site_name' => $siteName,
|
||||||
|
'site_desc' => ($siteDesc !== '' ? $siteDesc : (string)fb_cfg('site_desc', '')),
|
||||||
|
'admin_email' => $admEmail,
|
||||||
|
'base_url' => $baseUrl,
|
||||||
|
'api_token' => fb_install_rand(16),
|
||||||
|
'ip_salt' => fb_install_rand(16),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($cfgErr === '') {
|
||||||
|
fb_cfg_flush(); // 让后续取值看到刚落盘的新 config.php
|
||||||
|
} else {
|
||||||
|
// config.php 写不进去不算致命:settings 表里的值同样生效,先把系统跑起来并如实告知
|
||||||
|
$err = '提示:' . $cfgErr . '(站内设置已写入数据库,系统可用;但建议修好后手工补齐 config.php)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ③ 站点信息入库(优先生效,且不依赖 config.php 是否可写)
|
||||||
|
fb_set_setting('site_name', $siteName);
|
||||||
|
if ($siteDesc !== '') { fb_set_setting('site_desc', $siteDesc); }
|
||||||
|
fb_set_setting('installed_at', time());
|
||||||
|
|
||||||
|
// ④ 口令:只写 bcrypt 哈希,绝不落盘明文
|
||||||
|
$pwSet = false;
|
||||||
|
if ($setPw && fb_len($pw) >= 8 && $pw === $pw2) {
|
||||||
|
$pwSet = fb_admin_set_password($pw);
|
||||||
|
}
|
||||||
|
|
||||||
|
$done = true;
|
||||||
|
$GLOBALS['__fb_install_pw_set'] = $pwSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<title>安装 · 轻反馈 FeedLite</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
||||||
|
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
||||||
|
<link rel="stylesheet" href="assets/style.css?v=7">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="wrap">
|
||||||
|
<span class="brand-mini">
|
||||||
|
<span class="logo-mark" role="img" aria-label="轻反馈图标"></span>
|
||||||
|
<span class="brand-text">轻反馈 FeedLite</span>
|
||||||
|
</span>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<span class="navlink hide-sm">安装向导</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="wrap">
|
||||||
|
|
||||||
|
<?php if ($already && !$done): ?>
|
||||||
|
<section class="narrow">
|
||||||
|
<h1>已经装好了</h1>
|
||||||
|
<p class="muted">检测到数据库已存在,无需重复安装。</p>
|
||||||
|
<p>
|
||||||
|
<a class="btn primary" href="index.php">进入问题列表</a>
|
||||||
|
<a class="btn" href="admin.php">管理后台</a>
|
||||||
|
</p>
|
||||||
|
<div class="card-form" style="margin-top:18px">
|
||||||
|
<p class="muted small">如果你确实想重装(例如换了数据库位置),请先手动移走
|
||||||
|
<code>data/feedback.db</code> 再刷新本页。本向导不会替你删除任何数据。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php elseif ($done): ?>
|
||||||
|
<?php $pwSet = !empty($GLOBALS['__fb_install_pw_set']); ?>
|
||||||
|
<section class="narrow">
|
||||||
|
<h1>安装完成 🎉</h1>
|
||||||
|
<p class="muted">已生成 <code>config.php</code>、建好数据库表。下面是接下来要做的两件事:</p>
|
||||||
|
|
||||||
|
<div class="card-form" style="margin:14px 0">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
|
<span><b>① 删除安装文件</b> —— 请把这个 <code>install.php</code> 从服务器上删掉(已安装态下它会拒绝重装,但删掉更干净)。</span>
|
||||||
|
<span><b>② 设置口令</b> ——
|
||||||
|
<?php if ($pwSet): ?>
|
||||||
|
管理员口令已设成你刚才填的那个,并以 <b>bcrypt 哈希</b>存进数据库;<code>config.php</code> 里没有任何明文口令。
|
||||||
|
<?php else: ?>
|
||||||
|
你这次没设口令。<b>在设好之前后台无法登录</b>:请进入 <a href="admin.php">管理后台</a>,在「站点设置 → 登录口令」里设定(或重新运行本向导)。
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($err !== ''): ?><?php fb_flash_box($err, 'error'); ?><?php endif; ?>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a class="btn primary" href="index.php">进入问题列表</a>
|
||||||
|
<a class="btn" href="admin.php">管理后台</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<?php list($envOk, $rows) = fb_install_checks(); ?>
|
||||||
|
|
||||||
|
<div class="page-head">
|
||||||
|
<div>
|
||||||
|
<h1>安装向导</h1>
|
||||||
|
<p class="muted">数据库目录 <code>data/</code> 下还没有数据,先完成这几步设置,装完即可使用。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$envOk): ?>
|
||||||
|
<?php fb_flash_box('环境体检未通过:请先处理下方标红项,处理好后刷新本页。', 'error'); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<h2 style="margin:22px 0 10px">一、环境体检</h2>
|
||||||
|
<div class="card-form" style="margin:0 0 18px">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px">
|
||||||
|
<?php foreach ($rows as $r): ?>
|
||||||
|
<span>
|
||||||
|
<?php echo $r[2] ? '✅' : '❌'; ?>
|
||||||
|
<b><?php echo e($r[0]); ?></b>:<?php echo e($r[1]); ?>
|
||||||
|
<?php if (!empty($r[3])): ?><i class="hint">(<?php echo e($r[3]); ?>)</i><?php endif; ?>
|
||||||
|
<?php if (!empty($r[4])): ?><br><span class="muted small" style="margin-left:1.4em">↳ <?php echo e($r[4]); ?></span><?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="margin:22px 0 10px">二、填写站点信息</h2>
|
||||||
|
<?php fb_flash_box($err, 'error'); ?>
|
||||||
|
<form method="post" action="install.php" class="card-form">
|
||||||
|
<input type="hidden" name="csrf" value="<?php echo e(fb_csrf_token()); ?>">
|
||||||
|
|
||||||
|
<label class="field"><span>站点标题 <b class="req">*</b></span>
|
||||||
|
<input type="text" name="site_name" maxlength="80" required
|
||||||
|
value="<?php echo e(isset($_POST['site_name']) ? (string)$_POST['site_name'] : (string)fb_cfg('site_name', '轻反馈')); ?>"
|
||||||
|
placeholder="轻反馈">
|
||||||
|
<i class="hint">显示在浏览器标签、页面顶栏与页脚。用来说明「本系统为哪个软件提供反馈服务」,如「XX 软件 · 问题反馈」。</i></label>
|
||||||
|
|
||||||
|
<label class="field"><span>站点描述</span>
|
||||||
|
<textarea name="site_desc" rows="3" maxlength="300"
|
||||||
|
placeholder="欢迎在这里反馈使用问题、提出功能建议。"><?php echo e(isset($_POST['site_desc']) ? (string)$_POST['site_desc'] : (string)fb_cfg('site_desc', '')); ?></textarea>
|
||||||
|
<i class="hint">显示在首页标题下方,一句话说明这里可以反馈什么。</i></label>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<label class="field"><span>官方邮箱</span>
|
||||||
|
<input type="email" name="admin_email" value="<?php echo e(isset($_POST['admin_email']) ? (string)$_POST['admin_email'] : (string)fb_cfg('admin_email', '')); ?>"
|
||||||
|
placeholder="support@example.com">
|
||||||
|
<i class="hint">对外展示用的联系邮箱,可留空。</i></label>
|
||||||
|
<label class="field"><span>站点根地址</span>
|
||||||
|
<input type="text" name="base_url" value="<?php echo e(isset($_POST['base_url']) ? (string)$_POST['base_url'] : (string)fb_cfg('base_url', '')); ?>"
|
||||||
|
placeholder="https://你的域名/feedback">
|
||||||
|
<i class="hint">用于邮件正文里的链接。留空则按访问域名自动探测。</i></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="margin:22px 0 10px">三、设置管理员口令</h2>
|
||||||
|
<p class="muted small" style="margin:-4px 0 10px">
|
||||||
|
口令用来登录管理后台、以及以「官方」身份回复。<b>它以 bcrypt 单向哈希写入数据库,明文不会保存在 config.php 或任何文件里。</b>
|
||||||
|
留空则暂不设定,装完后可在后台补设(补设之前后台不可登录)。
|
||||||
|
</p>
|
||||||
|
<div class="grid2">
|
||||||
|
<label class="field"><span>管理员口令(至少 8 位)</span>
|
||||||
|
<input type="password" name="admin_password" autocomplete="new-password"></label>
|
||||||
|
<label class="field"><span>再输入一次</span>
|
||||||
|
<input type="password" name="admin_password2" autocomplete="new-password"></label>
|
||||||
|
</div>
|
||||||
|
<label class="check"><input type="checkbox" name="set_password" value="1" checked> 现在设定口令(推荐)</label>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn primary" type="submit" name="do_install" value="1"<?php echo $envOk ? '' : ' disabled'; ?>>开始安装</button>
|
||||||
|
</div>
|
||||||
|
<?php if (!$envOk): ?>
|
||||||
|
<p class="muted small">环境体检未通过前无法提交,请先处理标红项。</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="card-form" style="margin-top:18px">
|
||||||
|
<p class="muted small">安装后再删掉本文件即可。<br>
|
||||||
|
邮件通知(SMTP)与附件上传(阿里云 OSS)都在安装完成后、在后台「站点设置」里配置,这里不需要填。</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="wrap">
|
||||||
|
<p class="muted foot-line"><span>轻反馈 FeedLite</span><span class="dot">·</span><a href="<?php echo e(fb_credit_url_default()); ?>" target="_blank" rel="noopener">FeedLite</a></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+51
-11
@@ -29,6 +29,30 @@ if (!isset($GLOBALS['__fb_cfg_stamp'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!function_exists('fb_cfg')) {
|
if (!function_exists('fb_cfg')) {
|
||||||
|
/**
|
||||||
|
* 只读【配置文件】的值(config.php → config.example.php),
|
||||||
|
* **不叠加 settings 表、绝不触达数据库**。
|
||||||
|
*
|
||||||
|
* 为什么必须单独有这么一条通道:正常的 fb_cfg() 会去读 settings 表,
|
||||||
|
* 而读表要先 fb_db() —— 那一步会就地建出一个空数据库文件。
|
||||||
|
* 「是否已安装」的判定恰恰以「数据库文件在不在」为依据,
|
||||||
|
* 所以判定必须走这条不碰库的路径,否则第一秒就把安装态自己造出来了。
|
||||||
|
*/
|
||||||
|
function fb_cfg_file() {
|
||||||
|
static $file = null;
|
||||||
|
static $stamp = -1;
|
||||||
|
$now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0;
|
||||||
|
if ($file === null || $stamp !== $now) {
|
||||||
|
$local = FB_ROOT . '/config.php';
|
||||||
|
$example = FB_ROOT . '/config.example.php';
|
||||||
|
if (is_file($local)) { $file = (array)(require $local); }
|
||||||
|
elseif (is_file($example)) { $file = (array)(require $example); }
|
||||||
|
else { $file = []; }
|
||||||
|
$stamp = $now;
|
||||||
|
}
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取配置(点分路径,如 fb_cfg('smtp.host'))
|
* 读取配置(点分路径,如 fb_cfg('smtp.host'))
|
||||||
* 取值顺序:settings 表(后台改过的)> config.php(本地配置)> config.example.php(出厂默认)> $default
|
* 取值顺序:settings 表(后台改过的)> config.php(本地配置)> config.example.php(出厂默认)> $default
|
||||||
@@ -41,15 +65,7 @@ if (!function_exists('fb_cfg')) {
|
|||||||
static $stamp = -1;
|
static $stamp = -1;
|
||||||
$now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0;
|
$now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0;
|
||||||
if ($merged === null || $stamp !== $now) {
|
if ($merged === null || $stamp !== $now) {
|
||||||
$local = FB_ROOT . '/config.php';
|
$cfg = fb_cfg_file();
|
||||||
$example = FB_ROOT . '/config.example.php';
|
|
||||||
if (is_file($local)) {
|
|
||||||
$cfg = (array)(require $local);
|
|
||||||
} elseif (is_file($example)) {
|
|
||||||
$cfg = (array)(require $example);
|
|
||||||
} else {
|
|
||||||
$cfg = [];
|
|
||||||
}
|
|
||||||
// 后台设置以嵌套数组形式叠加在文件配置之上(只覆盖同名键)
|
// 后台设置以嵌套数组形式叠加在文件配置之上(只覆盖同名键)
|
||||||
$tree = function_exists('fb_setting_tree') ? fb_setting_tree() : [];
|
$tree = function_exists('fb_setting_tree') ? fb_setting_tree() : [];
|
||||||
$merged = is_array($tree) && $tree ? array_replace_recursive($cfg, $tree) : $cfg;
|
$merged = is_array($tree) && $tree ? array_replace_recursive($cfg, $tree) : $cfg;
|
||||||
@@ -68,6 +84,11 @@ if (!function_exists('fb_cfg')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 让 fb_cfg() 的进程内缓存失效(改了磁盘上的 config.php 后必须调用) */
|
||||||
|
function fb_cfg_flush() {
|
||||||
|
$GLOBALS['__fb_cfg_stamp'] = ((int)$GLOBALS['__fb_cfg_stamp']) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
require_once FB_ROOT . '/lib/util.php';
|
require_once FB_ROOT . '/lib/util.php';
|
||||||
require_once FB_ROOT . '/lib/db.php';
|
require_once FB_ROOT . '/lib/db.php';
|
||||||
require_once FB_ROOT . '/lib/oss.php';
|
require_once FB_ROOT . '/lib/oss.php';
|
||||||
@@ -76,6 +97,20 @@ require_once FB_ROOT . '/lib/captcha.php';
|
|||||||
require_once FB_ROOT . '/lib/mailer.php';
|
require_once FB_ROOT . '/lib/mailer.php';
|
||||||
require_once FB_ROOT . '/lib/layout.php';
|
require_once FB_ROOT . '/lib/layout.php';
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
* 安装引导:数据库还没生成(data/ 下没数据)就说明是全新部署,
|
||||||
|
* 一律先送去 install.php 填配置、建库、设口令,装完才进系统。
|
||||||
|
* 判据只看「数据库文件在不在」——绝不能在这里调 fb_db(),
|
||||||
|
* 因为那会就地建出一个空库、把安装态自己"装"出来,之后就再也进不了向导。
|
||||||
|
* ------------------------------------------------------------------ */
|
||||||
|
if (!defined('FB_SKIP_INSTALL_REDIRECT') && !fb_installed()) {
|
||||||
|
if (PHP_SAPI === 'cli') {
|
||||||
|
fwrite(STDERR, "FeedLite 尚未安装:请先在浏览器中打开本目录下的 install.php 完成安装。\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
fb_redirect('install.php');
|
||||||
|
}
|
||||||
|
|
||||||
// 会话(CLI 下不启用)
|
// 会话(CLI 下不启用)
|
||||||
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {
|
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
$p = session_get_cookie_params();
|
$p = session_get_cookie_params();
|
||||||
@@ -91,5 +126,10 @@ if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {
|
|||||||
@session_start();
|
@session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据库(建表)
|
// 初始化数据库(建表)。
|
||||||
fb_db();
|
// ★ 只在【已安装】时做:未安装就建库,等于第一秒把这个状态自己抹掉,
|
||||||
|
// 安装向导(install.php)随后就会被判定成「已经装好了」而拒绝工作。
|
||||||
|
// install.php 在真正提交安装时会自己显式调用 fb_db()。
|
||||||
|
if (fb_installed()) {
|
||||||
|
fb_db();
|
||||||
|
}
|
||||||
|
|||||||
+15
-3
@@ -27,7 +27,7 @@ function fb_layout_head($title, array $opt = []) {
|
|||||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="assets/apple-touch-icon.png">
|
||||||
<link rel="manifest" href="assets/site.webmanifest">
|
<link rel="manifest" href="assets/site.webmanifest">
|
||||||
<link rel="stylesheet" href="assets/style.css?v=6">
|
<link rel="stylesheet" href="assets/style.css?v=7">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@@ -48,10 +48,22 @@ function fb_layout_head($title, array $opt = []) {
|
|||||||
|
|
||||||
/** 输出页面底部 */
|
/** 输出页面底部 */
|
||||||
function fb_layout_foot() {
|
function fb_layout_foot() {
|
||||||
?><footer class="foot">
|
// 页脚两件事:① 常驻「管理后台」入口(省得每次手敲 admin.php);② 可关闭的 FeedLite 版权句。
|
||||||
|
// 版权句里的 FeedLite 指向开源仓库,地址可后台改;关掉开关则整句不渲染,只留管理后台链接。
|
||||||
|
$showCredit = (bool)fb_cfg('footer.show_credit', true);
|
||||||
|
$creditUrl = trim((string)fb_cfg('footer.credit_url', ''));
|
||||||
|
if ($creditUrl === '') { $creditUrl = fb_credit_url_default(); }
|
||||||
|
// 只放行 http/https:防止把 javascript:/data: 之类写进页脚链接(后台设置项,同样要防)
|
||||||
|
if (!preg_match('~^https?://~i', $creditUrl)) { $creditUrl = fb_credit_url_default(); }
|
||||||
|
?>
|
||||||
|
<footer class="foot">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<p class="muted">本站仅保存你提交的昵称、邮箱与内容,用于问题跟进;页面不会公开展示邮箱与手机号。</p>
|
<p class="muted">本站仅保存你提交的昵称、邮箱与内容,用于问题跟进;页面不会公开展示邮箱与手机号。</p>
|
||||||
<p class="muted"><?php echo e((string)fb_cfg('site_name', '轻反馈')); ?> · 由 FeedLite 驱动</p>
|
<p class="muted foot-line">
|
||||||
|
<?php if ($showCredit): ?><span><?php echo e((string)fb_cfg('site_name', '轻反馈')); ?> · 由 <a href="<?php echo e($creditUrl); ?>" target="_blank" rel="noopener">FeedLite</a> 驱动</span><?php endif; ?>
|
||||||
|
<?php if ($showCredit): ?><span class="dot">·</span><?php endif; ?>
|
||||||
|
<a href="admin.php">管理后台</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="assets/app.js?v=5"></script>
|
<script src="assets/app.js?v=5"></script>
|
||||||
|
|||||||
+53
-3
@@ -155,6 +155,24 @@ function fb_thread_public(array $thread) {
|
|||||||
return fb_is_admin();
|
return fb_is_admin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- 安装状态 ---------------- */
|
||||||
|
|
||||||
|
/** FeedLite 开源仓库地址(页脚版权链接的默认值,可在后台改) */
|
||||||
|
function fb_credit_url_default() {
|
||||||
|
return 'https://code.yangyq.net/laoyang/FeedLite';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已安装:数据库文件存在且非空。
|
||||||
|
* ★ 必须用 fb_cfg_file()(纯文件配置)而不是 fb_cfg():
|
||||||
|
* 后者会去读 settings 表 -> 触发 fb_db() -> 就地建出一个空库,
|
||||||
|
* 那样第二秒就永远进不去安装向导了(安装态会被自己"装"出来)。
|
||||||
|
*/
|
||||||
|
function fb_installed() {
|
||||||
|
$p = (string)fb_cfg_file()['db_path'] ?? '';
|
||||||
|
return ($p !== '' && is_file($p) && (int)@filesize($p) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 站点设置(数据库覆盖 config.php) ----------------
|
/* ---------------- 站点设置(数据库覆盖 config.php) ----------------
|
||||||
* 后台改的设置存在 settings 表,读取时覆盖 config.php 的同名项;
|
* 后台改的设置存在 settings 表,读取时覆盖 config.php 的同名项;
|
||||||
* config.php 因此变成「出厂默认值」,部署后不必再手工改它。
|
* config.php 因此变成「出厂默认值」,部署后不必再手工改它。
|
||||||
@@ -164,6 +182,8 @@ function fb_thread_public(array $thread) {
|
|||||||
function fb_setting_keys() {
|
function fb_setting_keys() {
|
||||||
return [
|
return [
|
||||||
'site_name', 'site_desc',
|
'site_name', 'site_desc',
|
||||||
|
'footer.show_credit', 'footer.credit_url',
|
||||||
|
'installed_at',
|
||||||
'attachment.storage',
|
'attachment.storage',
|
||||||
'upload.image_max_per_post', 'upload.image_max_bytes',
|
'upload.image_max_per_post', 'upload.image_max_bytes',
|
||||||
'upload.video_max_per_post', 'upload.video_max_bytes',
|
'upload.video_max_per_post', 'upload.video_max_bytes',
|
||||||
@@ -187,6 +207,10 @@ function fb_settings_flat($reload = false) {
|
|||||||
static $loading = false;
|
static $loading = false;
|
||||||
if ($flat !== null && !$reload) { return $flat; }
|
if ($flat !== null && !$reload) { return $flat; }
|
||||||
if ($loading) { return []; }
|
if ($loading) { return []; }
|
||||||
|
/* ★ 未安装时绝不读表:读表要先 fb_db(),那会就地建出一个空数据库文件,
|
||||||
|
* 等于把「未安装」这个状态自己抹掉 —— 安装向导就再也进不去了。
|
||||||
|
* fb_installed() 走纯文件配置、不碰库,可以安全地在这里调用。 */
|
||||||
|
if (function_exists('fb_installed') && !fb_installed()) { return []; }
|
||||||
$loading = true;
|
$loading = true;
|
||||||
$out = [];
|
$out = [];
|
||||||
try {
|
try {
|
||||||
@@ -280,19 +304,45 @@ function fb_admin_login() {
|
|||||||
@session_regenerate_id(true);
|
@session_regenerate_id(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 安装向导会直接写成对的占位值;这些值一律视为「没设口令」 */
|
||||||
|
function fb_admin_password_placeholder() {
|
||||||
|
return ['', 'CHANGE_ME_admin', 'CHANGE_ME', 'change_me', 'admin', 'password'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 口令当前存在哪 —— 用于在后台如实告诉管理员「口令保存在哪里、是否加密」。
|
||||||
|
* ① 'db' :数据库 settings 表的 admin.password_hash,bcrypt 单向哈希(不可逆)
|
||||||
|
* ② 'file' :config.php 的 admin_password 明文(仅安装向导之前的老部署会走到)
|
||||||
|
* ③ 'none' :尚未设置(或仍是出厂占位值)→ 后台拒绝登录,只能用安装向导设一个
|
||||||
|
*/
|
||||||
|
function fb_admin_password_source() {
|
||||||
|
if ((string)fb_setting('admin.password_hash', '') !== '') { return 'db'; }
|
||||||
|
$cfg = trim((string)fb_cfg('admin_password', ''));
|
||||||
|
if ($cfg === '' || in_array($cfg, fb_admin_password_placeholder(), true)) { return 'none'; }
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否已设置过可用口令(未设置时后台不可登录,且不提示"口令不正确"而是提示去安装) */
|
||||||
|
function fb_admin_password_is_set() {
|
||||||
|
return fb_admin_password_source() !== 'none';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验管理员口令。
|
* 校验管理员口令。
|
||||||
* 后台改过口令后存的是 password_hash;没改过就用 config.php 的 admin_password。
|
* 后台改过口令后存的是 password_hash;没改过才回落到 config.php 的明文。
|
||||||
|
* ★ 出厂占位值(CHANGE_ME_admin 等)一律拒绝:否则 clone 下来没改配置的站点,
|
||||||
|
* 任何人都能用文档里公开的占位口令登进后台。
|
||||||
*/
|
*/
|
||||||
function fb_admin_verify_password($pw) {
|
function fb_admin_verify_password($pw) {
|
||||||
$pw = (string)$pw;
|
$pw = (string)$pw;
|
||||||
$hash = (string)fb_setting('admin.password_hash', '');
|
$hash = (string)fb_setting('admin.password_hash', '');
|
||||||
if ($hash !== '') { return password_verify($pw, $hash); }
|
if ($hash !== '') { return password_verify($pw, $hash); }
|
||||||
$cfg = (string)fb_cfg('admin_password', '');
|
$cfg = (string)fb_cfg('admin_password', '');
|
||||||
return $cfg !== '' && hash_equals($cfg, $pw);
|
if ($cfg === '' || in_array($cfg, fb_admin_password_placeholder(), true)) { return false; }
|
||||||
|
return hash_equals($cfg, $pw);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改管理员口令(存哈希到数据库,立即生效) */
|
/** 修改管理员口令(存 bcrypt 哈希到数据库,立即生效) */
|
||||||
function fb_admin_set_password($pw) {
|
function fb_admin_set_password($pw) {
|
||||||
return fb_set_setting('admin.password_hash', password_hash((string)$pw, PASSWORD_DEFAULT));
|
return fb_set_setting('admin.password_hash', password_hash((string)$pw, PASSWORD_DEFAULT));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user