安装完成后自动删除 install.php(不再让用户手动删)
用户要求:装完由程序自己删,不该让用户记着。 三级降级(删不掉也必须安全、且如实告知): ① 目录可写 → unlink 删除 ② 删不掉 → 覆写成**纯静态 HTML** 404 页(不含 <?php) ③ 都失败 → 成功页明确提示需手动删 + 原因,不谎报成功 关键实现要点: - 自删除放在安装 try/catch **之外**(if ($done))。它是清理动作, 不属于安装事务;若留在 try 里抛异常会被 catch 误报成「安装失败」。 - 降级**不能写成 PHP 空壳**:实测 opcache / php -S 会缓存已编译脚本, 写成 `<?php exit;` 后请求仍返回旧的「已安装」页(2289 字节), 等于文件还是暴露安装页的入口。纯静态 HTML 才彻底。 - 补 opcache_invalidate,让残留文件立刻按新内容生效。 测试: - _sn_selfdelete_e2e.py 19 项:删成功 / 目录不可写降级 / 删除失败不影响安装 - _sn_selfdelete_falsify.py 6 变体全精确命中 (含「不自删但谎报」「降级写成 PHP」「删除失败就回滚」) - install e2e 41 项、跳转 e2e 19 项回归全绿(适配自删除后的语义变化) 顺手修 e2e 自身缺陷:install.php 会被自删除吃掉, 快照改为存放在仓库内 .install.php.e2e_snapshot 并**无条件还原**。
This commit is contained in:
+88
-1
@@ -86,6 +86,60 @@ foreach ($checks as $ck) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 装完**自己删除自己**。
|
||||
*
|
||||
* 为什么不交给用户手动删:用户会忘。留着的 install.php 即使有「已有管理员就拒绝执行」
|
||||
* 的守卫,也仍是攻击面(万一哪天守卫被改坏、或数据库被清空,就能重装夺权)。
|
||||
* 能自动做的事不该让人记着。
|
||||
*
|
||||
* 三级降级(删不掉也必须让站是安全的、且如实告知):
|
||||
* ① 目录可写 → unlink 删掉,最干净。
|
||||
* ② 目录不可写但文件可写 → 把文件内容**截断成无意义的空壳**;
|
||||
* 访问它只会得到空响应,同样失去威胁(这是没法删时的次优解)。
|
||||
* ③ 都失败 → 返回原因,页面明确提示「请手动删除」,绝不谎报成功。
|
||||
*
|
||||
* @return array{ok:bool, mode:string, msg:string} mode: deleted|blanked|failed
|
||||
*/
|
||||
function sn_self_delete(string $file): array
|
||||
{
|
||||
if (!is_file($file)) {
|
||||
return ['ok' => true, 'mode' => 'deleted', 'msg' => '文件已不存在'];
|
||||
}
|
||||
|
||||
// ① 正常删除(关键在**所在目录**是否可写,与文件自身权限无关)
|
||||
if (@unlink($file)) {
|
||||
return ['ok' => true, 'mode' => 'deleted', 'msg' => '已自动删除'];
|
||||
}
|
||||
|
||||
// ② 删不掉 → 覆写成**纯静态 HTML**(关键:不能写成 PHP)
|
||||
//
|
||||
// ⚠️ 实测教训:原先写成 `<?php ... exit;` 的空壳,**运行时无效** ——
|
||||
// opcache / php -S 会缓存已编译的脚本,请求照样返回旧内容(实测 2289 字节的
|
||||
// 「已安装」页),文件仍是暴露安装页的入口。
|
||||
// 纯静态 HTML 不经过 PHP 编译,不做任何事,也不存在被缓存成旧逻辑的问题。
|
||||
$blank = "<!doctype html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\">"
|
||||
. "<title>404</title></head><body><h1>404</h1>"
|
||||
. "<p>安装已完成,此文件已失效,可安全删除。</p></body></html>\n";
|
||||
if (@file_put_contents($file, $blank, LOCK_EX) !== false) {
|
||||
// 清掉可能存在的编译缓存,让残留文件立刻按新内容(静态)生效
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
@opcache_invalidate($file, true);
|
||||
}
|
||||
return ['ok' => true, 'mode' => 'blanked',
|
||||
'msg' => '目录不可写,已覆写为无害静态页(无法执行任何代码,但仍建议手动删除)'];
|
||||
}
|
||||
|
||||
// ③ 彻底失败,如实上报
|
||||
$why = '未知原因';
|
||||
if (!is_writable(dirname($file))) {
|
||||
$why = '目录 ' . dirname($file) . ' 不可写';
|
||||
} elseif (!is_writable($file)) {
|
||||
$why = '文件本身不可写';
|
||||
}
|
||||
return ['ok' => false, 'mode' => 'failed', 'msg' => '无法删除:' . $why];
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* 安装状态
|
||||
* ========================================================================= */
|
||||
@@ -97,6 +151,7 @@ $installed = is_file($lockFile) || $hasAdmin;
|
||||
|
||||
$err = '';
|
||||
$done = false;
|
||||
$delResult = null; // 自删除结果(仅安装成功那一轮有值)
|
||||
|
||||
/* =========================================================================
|
||||
* 执行安装
|
||||
@@ -145,6 +200,17 @@ if (!$installed && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} catch (Throwable $e) {
|
||||
$err = '安装失败:' . $e->getMessage();
|
||||
}
|
||||
|
||||
// 自删除**刻意放在 try/catch 之外**。
|
||||
//
|
||||
// 为什么:它是一次「清理动作」,不属于安装事务。如果在 try 里抛异常,
|
||||
// 会被上面 catch 兜住并写进 $err —— 而那时账号其实已经建好了,
|
||||
// 等于把「清理失败」误报成「安装失败」。
|
||||
// sn_self_delete 内部全部用 @ 抑制且返回结构化结果,本身不会抛;
|
||||
// 放在外面是双重保险,语义也更清楚。
|
||||
if ($done) {
|
||||
$delResult = sn_self_delete(__FILE__);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +264,9 @@ $title = $installed ? '已安装' : '安装向导';
|
||||
.dbinfo dd{margin:0;font-variant-numeric:tabular-nums}
|
||||
.tip{font-size:12px;line-height:1.7;color:var(--muted,#777);margin:14px 0 0}
|
||||
.tip strong{color:#c62828}
|
||||
.ok-tip strong{color:#128a3c}
|
||||
.warn-tip strong{color:#b26a00}
|
||||
.warn-tip code,.ok-tip code{background:rgba(128,128,128,.15);padding:1px 5px;border-radius:4px}
|
||||
</style>
|
||||
</head>
|
||||
<body class="admin">
|
||||
@@ -245,8 +314,26 @@ $title = $installed ? '已安装' : '安装向导';
|
||||
|
||||
<a class="btn primary block" href="<?= e(url('admin/')) ?>">进入后台</a>
|
||||
|
||||
<?php if ($delResult !== null): ?>
|
||||
<?php if ($delResult['ok']): ?>
|
||||
<p class="tip ok-tip">
|
||||
<strong>✓ 安装文件已自动处理:<?= e($delResult['msg']) ?></strong><br>
|
||||
你不需要再做任何清理。
|
||||
<?php if ($delResult['mode'] === 'blanked'): ?>
|
||||
<br>(目录不可写所以没能物理删除,内容已清空成无害空壳;若想彻底删掉,
|
||||
请给站点根目录写权限后手动 <code>rm install.php</code>。)
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p class="tip warn-tip">
|
||||
<strong>⚠ 安装文件没能自动删除:<?= e($delResult['msg']) ?></strong><br>
|
||||
站点照常可用,但请手动执行:<code>rm <?= e(APP_PATH) ?>/install.php</code><br>
|
||||
原因通常是站点根目录对 PHP 进程不可写。
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="tip">
|
||||
<strong>下一步(重要):把服务器上的 install.php 删掉。</strong><br>
|
||||
后台默认已有一份示例板块和示例网站,进去之后可以随意删改、添加自己的链接。
|
||||
</p>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user