diff --git a/install.php b/install.php index f33d2cb..51162e3 100755 --- a/install.php +++ b/install.php @@ -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) + // + // ⚠️ 实测教训:原先写成 `\n
" + . "安装已完成,此文件已失效,可安全删除。
\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} @@ -245,8 +314,26 @@ $title = $installed ? '已安装' : '安装向导'; 进入后台 + + +
+ ✓ 安装文件已自动处理:= e($delResult['msg']) ?>
+ 你不需要再做任何清理。
+
+
(目录不可写所以没能物理删除,内容已清空成无害空壳;若想彻底删掉,
+ 请给站点根目录写权限后手动 rm install.php。)
+
+
+ ⚠ 安装文件没能自动删除:= e($delResult['msg']) ?>
+ 站点照常可用,但请手动执行:rm = e(APP_PATH) ?>/install.php
+ 原因通常是站点根目录对 PHP 进程不可写。
+
- 下一步(重要):把服务器上的 install.php 删掉。
后台默认已有一份示例板块和示例网站,进去之后可以随意删改、添加自己的链接。