安装向导:装完自动删除 install.php;站点根只读不再阻断安装 (#1 #2)

## 装完自删
install.php 是可写文件,留着等于长期暴露一个「重装入口」。
现在安装成功后自动 unlink 自己,不再要用户手动删。

两种结果都如实告知,不假装:
- 删除成功 → 成功页显示「已自动删除」
- 删不掉   → 显示「没能自动删除」+ 具体原因(目录不可写 / 属主不符)+ 手工处理命令
安装本身不受影响:清理失败不算安装失败。

## 🔴 修掉一个越权动作(本轮最要紧的修改)
初版为了「尽量删掉」,先 @chmod(dirname($self), 0755) 再 unlink。
这等于程序自己把「站点根只读」这个安全设置拆掉了 —— 管理员有意设成只读的目录
被 PHP 改回可写,然后文件就被删了。既违反管理员意图,也让「删不掉」这个真实场景
永远测不出来(我第一版测试就是这么被骗过去的)。

删除失败就如实报告,不准硬删。

## 站点根只读不再阻断安装
原先环境体检把「站点根可写」列为硬性要求,只读就红叉、禁止提交。
但这没道理:config.php 写不进去时,设置会全部落到数据库 settings 表,功能一样。
反过来,站点根只读是更安全的做法。

现在 data/ 与 uploads/ 可写仍是硬要求,站点根只读只显示为提示(不影响判定)。

## rename 兜底收窄
只在目录可写时才试 rename(目录不可写时 rename 同样失败,
硬试会在站点里留下 .deleted.xxxx 垃圾)。改名成功但删不掉则还原。

验证:
- 自删 A 场景(目录可写)→ 文件已删,页面提示「已自动删除」
- 自删 B 场景(目录只读)→ 文件保留,提示「目录对 Web 用户不可写」,安装仍成功
- 全新安装 33/33、老基线 25/25、附件专项 22/22、安装向导 60/60
This commit is contained in:
2026-11-01 12:55:08 +08:00
parent 7a578fac47
commit 1c2e8d5e65
+85 -7
View File
@@ -44,8 +44,10 @@ function fb_install_checks() {
],
];
// 只有 data/ 与 uploads/ 是硬要求(数据库和附件必须能写)。
// 站点根不必可写:config.php 写不进去时,设置会落到数据库 settings 表,
// 站点照常运行。反过来,把站点根设成只读是更安全的做法,不该被判为错误。
$dirs = [
FB_ROOT => '站点根目录(要写入 config.php)',
FB_ROOT . '/data' => '数据库目录 data/',
FB_ROOT . '/uploads' => '本机附件目录 uploads/(本机存储模式用)',
];
@@ -82,13 +84,25 @@ function fb_install_checks() {
$rows[] = ['目录可写 ' . $shown, $w ? '可写' : '不可写', $w, $why, $w ? '' : ('请给 Web 用户写权限,例如 chown www:www ' . $d)];
}
// config.php 若已存在(例如从模板复制过),必须可写才能就地更新
// 站点根:只提示,不阻断。
// 写不进 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');
if (!$w) { $ok = false; }
$rows[] = ['config.php 可写', $w ? '可写' : '不可写', $w, '需要就地更新配置', $w ? '' : '请给该文件写权限,例如 chown www:www config.php'];
$rows[] = ['config.php 可写', $w ? '可写' : '只读(不影响使用)', true,
'需要就地更新配置时才有用',
$w ? '' : '只读可接受:后台改的设置会存数据库,不写这个文件。'];
} else {
$rows[] = ['config.php', '尚不存在(将自动生成)', true, '由安装向导创建', ''];
$rows[] = ['config.php', '尚不存在(将自动生成)', true, '由安装向导创建(站点根只读时会跳过)', ''];
}
$ex = is_file(FB_ROOT . '/config.example.php');
@@ -231,9 +245,62 @@ if (isset($_POST['do_install'])) {
$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">
@@ -296,10 +363,21 @@ if (isset($_POST['do_install'])) {
<?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>
它在已安装状态下会拒绝重装,但删掉更干净。
<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>