Files
FeedLite/install.php
T
laoyang b835afcd04 data/uploads 改为由 PHP 自动创建并自补防护,不依赖部署账号建目录 (#1)
## 问题
部署时用 btops 建 data/ 与 uploads/,属主成了 btops:btops,Web 用户 www 写不进去,
安装向导直接报「目录不可写」,还得请人用 root 手工 chown —— 这不该让用户干。

## 修法:让 PHP 自己建
PHP 以 Web 用户身份运行,它建出来的目录属主天生就是 www,不需要任何 chown。
- 部署脚本不再创建 data/ uploads/(只建 assets/ lib/)
- 首次访问时 fb_db_init() 自动 mkdir data/,并立刻补防护文件
- uploads/ 由 fb_local_ensure_dir() 同样自动建 + 补防护
- 安装向导的环境体检 mkdir 之后也立刻补防护,不留空窗期

## 新增 data/ 防护文件自动生成
原先只有 uploads/ 有自动防护,data/ 没有 —— 数据库目录裸奔。
新增 fb_db_ensure_guard(),写 .htaccess(拒绝直读,禁 db/db-wal/db-shm/sqlite/php)
+ index.html 占位,带 `# fb-data-guard: vN` 版本标记,规则升级时整份重写。

## 顺带确认
站点根 .htaccess 已有全局兜底(拒绝 .db/.log/.md/.bak 等直读 + Options -Indexes),
即使某个目录暂时缺 .htaccess,/data/ 和 /data/feedback.db 也是 403。
现在是双保险,不依赖单点。

验证(WSL 真实环境):
- 全新安装 33/33、老基线 25/25、附件专项 22/22、安装向导 60/60
- data/ 与 uploads/ 均由 PHP 自动创建,属主即运行用户
- 两者 .htaccess 自动生成并带正确版本标记(v1 / v2)
2026-11-01 15:07:53 +08:00

483 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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' => '中文字数统计(可选)',
'fileinfo' => [
'按文件内容识别附件类型(仅本机存储模式用得上)',
'未启用(不影响安装;本机存储时只按扩展名核对)',
'建议开启。注意 OSS 存储模式的类型判定以对象存储返回的 Content-Type 为准,不经过本扩展',
],
];
// 只有 data/ 与 uploads/ 是硬要求(数据库和附件必须能写)。
// 站点根不必可写:config.php 写不进去时,设置会落到数据库 settings 表,
// 站点照常运行。反过来,把站点根设成只读是更安全的做法,不该被判为错误。
$dirs = [
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.1~8.5,已在 8.5 实测)', $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) {
$note = '未启用(不影响安装)';
$hint = '';
if (is_array($why)) {
$note = isset($why[1]) ? $why[1] : $note;
$hint = isset($why[2]) ? $why[2] : '';
$why = isset($why[0]) ? $why[0] : '';
}
$has = extension_loaded($ext);
$rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : $note, true, $why, $has ? '' : $hint];
}
foreach ($dirs as $d => $why) {
if (!is_dir($d)) { @mkdir($d, 0755, true); }
// 目录一建好就立刻补防护文件,不留"目录存在但无防护"的空窗期。
// data/ 用 fb_db_ensure_guard(),uploads/ 用 fb_local_ensure_dir(),
// 两者都在被保护目录里写 .htaccess + index.html。
if (basename($d) === 'data' && function_exists('fb_db_ensure_guard')) {
fb_db_ensure_guard($d);
} elseif (basename($d) === 'uploads' && function_exists('fb_local_ensure_dir')) {
fb_local_ensure_dir();
}
$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 时,站点设置会全部落到数据库 settings 表,功能不受影响。
$rootW = is_writable(FB_ROOT);
$rows[] = [
'目录可写(站点根)',
$rootW ? '可写' : '只读(不影响使用)',
true, // 不参与 $ok 判定
'用于生成 config.php;只读时设置改存数据库,功能一样',
$rootW ? '' : '站点根只读是可接受的(更安全)。若希望生成 config.php,再给它写权限。',
];
// config.php 若已存在(例如从模板复制过),只读也不影响运行,只影响能否就地更新
if (is_file(FB_ROOT . '/config.php')) {
$w = is_writable(FB_ROOT . '/config.php');
$rows[] = ['config.php 可写', $w ? '可写' : '只读(不影响使用)', true,
'需要就地更新配置时才有用',
$w ? '' : '只读可接受:后台改的设置会存数据库,不写这个文件。'];
} 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'), '/');
$admUser = $post('admin_user');
$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 ($admUser === '') {
$err = '请填写管理员用户名。';
} elseif (!preg_match('/^[A-Za-z0-9_.\-]{2,32}$/', $admUser)) {
$err = '管理员用户名只能用字母、数字、下划线、点、连字符,长度 2~32。';
} elseif (fb_len($pw) < 8) {
$err = '管理员口令至少 8 个字符。';
} elseif ($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;
fb_admin_set_username($admUser);
if (fb_len($pw) >= 8 && $pw === $pw2) {
$pwSet = fb_admin_set_password($pw);
}
$done = true;
$GLOBALS['__fb_install_pw_set'] = $pwSet;
/* ---- 装完自动删除本文件 ----
* 安装向导本身是可写文件,留着等于长期暴露一个"重装入口"。
* 虽然已安装态会拒绝重装,但最干净的做法是装完即自毁。
*
* 删除失败不算安装失败:站点已经装好了,只是清理动作没成。
* 失败原因通常是文件属主不是 Web 用户(部署账号上传的文件常属 btops),
* 这时把实际情况记到 $GLOBALS 里,成功页如实告知,不假装删掉了。
*/
$GLOBALS['__fb_install_selfdel'] = fb_install_self_delete();
}
}
/**
* 安装完成后删除自身。
* @return string '' = 已删除;非空 = 没删掉的原因(给成功页显示)
*/
function fb_install_self_delete() {
$self = __FILE__;
if (!is_file($self)) { return ''; } // 已经不在了
/* ① 直接尝试删除。
*
* 🔴 这里绝不能先 chmod 目录!曾经写成 @chmod(dirname($self), 0755) 再删,
* 等于程序自己把"站点根只读"这个安全设置拆掉了 —— 用户有意设成只读的目录
* 被 PHP 改回可写,然后文件就被删掉了。既违反管理员意图,也让"删不掉"
* 这个真实场景永远测不出来。
* 删不掉就如实报告,不要硬删。
*/
if (@unlink($self)) { return ''; }
// ② 兜底:只有在目录可写时才试 rename(目录不可写时 rename 同样会失败,
// 硬试只会在站点里留下 .deleted.xxxx 垃圾文件)
if (is_writable(dirname($self))) {
$tmp = $self . '.deleted.' . substr(md5((string)time() . random_int(1000, 9999)), 0, 8);
if (@rename($self, $tmp)) {
if (@unlink($tmp)) { return ''; }
// 改名成功但删不掉:还原,别留垃圾
@rename($tmp, $self);
}
}
// ③ 都没成,给出可操作的原因
$dir = dirname($self);
$uid = function_exists('posix_getuid') ? posix_getuid() : -1;
$own = @fileowner($self);
$msg = '自动删除失败';
if (!is_writable($dir)) {
$msg .= ':' . $dir . ' 目录对 Web 用户不可写';
}
if ($uid >= 0 && $own !== false && $own !== $uid) {
$msg .= ':文件属主(uid ' . (int)$own . ')与运行用户(uid ' . $uid . ')不同';
}
return $msg;
}
?>
<!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=<?php echo (int)FB_ASSET_V; ?>">
</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']);
$justUser = fb_admin_username();
?>
<meta http-equiv="refresh" content="2;url=index.php">
<section class="narrow">
<h1>安装完成</h1>
<p class="muted">已建好数据库、写入站点设置。<b>2 秒后自动进入首页</b>(也可点下方按钮)。</p>
<div class="card-form" style="margin:14px 0">
<div style="display:flex;flex-direction:column;gap:8px">
<span><b>网站名称</b>:<?php echo e((string)fb_cfg('site_name', '')); ?></span>
<span><b>管理员用户名</b>:<?php echo e($justUser !== '' ? $justUser : '(未设置)'); ?></span>
<span><b>登录入口</b>:<a href="admin.php">admin.php</a>(用户名 + 口令)</span>
<span><b>口令存放</b>:bcrypt 哈希,存于数据库;<code>config.php</code> 里没有任何明文口令。</span>
</div>
</div>
<?php if ($err !== ''): ?><?php fb_flash_box($err, 'error'); ?><?php endif; ?>
<div class="card-form" style="margin:14px 0">
<?php
$selfDel = isset($GLOBALS['__fb_install_selfdel']) ? (string)$GLOBALS['__fb_install_selfdel'] : '';
?>
<?php if ($selfDel === ''): ?>
<p class="muted small" style="margin:0">
<b>✅ <code>install.php</code> 已自动删除</b>,不会再有重复安装的入口。
</p>
<?php else: ?>
<p class="muted small" style="margin:0">
<b>⚠️ 安装已完成,但 <code>install.php</code> 没能自动删除。</b><br>
原因:<?php echo e($selfDel); ?>。<br>
它在已安装状态下会拒绝重装,但建议手工删掉,或在服务器上执行:<br>
<code>sudo chown www:www <?php echo e(__DIR__); ?>/install.php &amp;&amp; sudo rm <?php echo e(__DIR__); ?>/install.php</code>
</p>
<?php endif; ?>
</div>
<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()); ?>">
<input type="hidden" name="site_desc" value="<?php echo e(isset($_POST['site_desc']) ? (string)$_POST['site_desc'] : ''); ?>">
<input type="hidden" name="admin_email" value="<?php echo e(isset($_POST['admin_email']) ? (string)$_POST['admin_email'] : ''); ?>">
<input type="hidden" name="base_url" value="<?php echo e(isset($_POST['base_url']) ? (string)$_POST['base_url'] : ''); ?>">
<input type="hidden" name="set_password" value="1">
<label class="field"><span>网站名称 <b class="req">*</b></span>
<input type="text" name="site_name" maxlength="80" required autofocus
value="<?php echo e(isset($_POST['site_name']) ? (string)$_POST['site_name'] : (string)fb_cfg('site_name', '')); ?>"
placeholder="例如:我的软件 · 问题反馈">
<i class="hint">显示在浏览器标签、页面顶栏和页脚。</i></label>
<h2 style="margin:20px 0 10px">管理员账号</h2>
<p class="muted small" style="margin:-4px 0 12px">
用来登录管理后台、以「官方」身份回复。<b>口令以 bcrypt 单向哈希存进数据库,
明文不会写进 config.php 或任何文件。</b>
</p>
<label class="field"><span>用户名 <b class="req">*</b></span>
<input type="text" name="admin_user" maxlength="32" required
value="<?php echo e(isset($_POST['admin_user']) ? (string)$_POST['admin_user'] : 'admin'); ?>"
placeholder="admin" autocomplete="username">
<i class="hint">字母、数字、下划线、点、连字符,2~32 位。</i></label>
<div class="grid2">
<label class="field"><span>口令 <b class="req">*</b></span>
<input type="password" name="admin_password" minlength="8" required autocomplete="new-password"></label>
<label class="field"><span>再输入一次 <b class="req">*</b></span>
<input type="password" name="admin_password2" minlength="8" required autocomplete="new-password"></label>
</div>
<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)在装好后到后台「站点设置」里配,这里不用填。<br>
<b>装完请删掉 install.php</b>(它本身也会拒绝重复安装)。
</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>