安装完成后自动删除 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 = '';
|
$err = '';
|
||||||
$done = false;
|
$done = false;
|
||||||
|
$delResult = null; // 自删除结果(仅安装成功那一轮有值)
|
||||||
|
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
* 执行安装
|
* 执行安装
|
||||||
@@ -145,6 +200,17 @@ if (!$installed && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$err = '安装失败:' . $e->getMessage();
|
$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}
|
.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{font-size:12px;line-height:1.7;color:var(--muted,#777);margin:14px 0 0}
|
||||||
.tip strong{color:#c62828}
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="admin">
|
<body class="admin">
|
||||||
@@ -245,8 +314,26 @@ $title = $installed ? '已安装' : '安装向导';
|
|||||||
|
|
||||||
<a class="btn primary block" href="<?= e(url('admin/')) ?>">进入后台</a>
|
<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">
|
<p class="tip">
|
||||||
<strong>下一步(重要):把服务器上的 install.php 删掉。</strong><br>
|
|
||||||
后台默认已有一份示例板块和示例网站,进去之后可以随意删改、添加自己的链接。
|
后台默认已有一份示例板块和示例网站,进去之后可以随意删改、添加自己的链接。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user