*/ function sn_env_checks(): array { $c = []; $add = static function (string $name, bool $ok, string $detail, bool $required = true) use (&$c): void { $c[] = ['name' => $name, 'ok' => $ok, 'detail' => $detail, 'required' => $required]; }; $add('PHP 版本 ≥ 8.0', PHP_VERSION_ID >= 80000, '当前 ' . PHP_VERSION . '(' . PHP_SAPI . ')'); $add('PDO SQLite 扩展', extension_loaded('pdo_sqlite'), extension_loaded('pdo_sqlite') ? '已启用' : '未启用,无法建库(必需)'); $add('mbstring 扩展', extension_loaded('mbstring'), extension_loaded('mbstring') ? '已启用' : '未启用,中文长度校验会失效(必需)'); $add('GD 扩展', extension_loaded('gd'), extension_loaded('gd') ? '已启用' : '未启用,无法抓取/缩放网站图标;仍可安装,但图标只能显示首字母块', false); $add('网络抓取能力', function_exists('curl_init') || (bool)ini_get('allow_url_fopen'), function_exists('curl_init') ? 'curl 可用' : ((bool)ini_get('allow_url_fopen') ? '仅 allow_url_fopen' : '都没有,无法自动抓取网站名称/图标(建议开启)')); $add('fileinfo 扩展', extension_loaded('fileinfo'), extension_loaded('fileinfo') ? '已启用' : '未启用,自定义图标的类型识别会退化'); $dirs = [ DATA_PATH => '数据目录 data/(放 SQLite 数据库)', ICON_PATH => '网站图标目录 uploads/icons/', SITE_ICON_PATH => '站点图标目录 uploads/site/', ]; foreach ($dirs as $dir => $label) { $rel = ltrim(str_replace(APP_PATH, '', $dir), '/\\'); $add($label, sn_dir_writable($dir), sn_dir_writable($dir) ? ($rel . ' 可写') : ($rel . ' 不可写,请给 Web 用户写权限')); } // 这两项只提醒,不阻断安装:即使漏了也能装,但数据文件会被公网下载 $dataHt = is_file(DATA_PATH . '/.htaccess'); $add('data/ 已禁止 Web 访问', $dataHt, $dataHt ? 'data/.htaccess 存在' : '缺少 data/.htaccess,数据库可能被直接下载(强烈建议补上)', false); return $c; } $checks = sn_env_checks(); $envOk = true; $warnOnly = []; foreach ($checks as $ck) { if (!$ck['ok']) { if ($ck['required']) { $envOk = false; } else { $warnOnly[] = $ck['name']; } } } /* ========================================================================= * 安装状态 * ========================================================================= */ $lockFile = DATA_PATH . '/install.lock'; // 与前台共用同一个判断源(functions.php 的 app_installed),避免两边口径不一致 $hasAdmin = app_installed(); $installed = is_file($lockFile) || $hasAdmin; $err = ''; $done = false; /* ========================================================================= * 执行安装 * ========================================================================= */ if (!$installed && $_SERVER['REQUEST_METHOD'] === 'POST') { $siteName = trim((string)($_POST['site_name'] ?? '')); $u = trim((string)($_POST['username'] ?? '')); $p = (string)($_POST['password'] ?? ''); $p2 = (string)($_POST['password2'] ?? ''); if (!$envOk) { $err = '环境检查未全部通过,请先解决上面标红的问题'; } elseif ($u === '' || $p === '') { $err = '用户名和密码都不能为空'; } elseif (!preg_match('/^[A-Za-z0-9_.\-]{2,32}$/', $u)) { $err = '用户名只能用字母、数字、下划线、点、连字符,长度 2~32'; } elseif (mb_strlen($p) < 6) { $err = '密码至少 6 位(建议 8 位以上,混合大小写和数字)'; } elseif ($p !== $p2) { $err = '两次输入的密码不一致'; } else { try { $pdo = db(); // 触发建表 + 写入默认设置/搜索引擎/示例板块 // 双保险:并发下第二个请求别重复创建 if ((int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0) { throw new RuntimeException('已有管理员账号,安装已中止。请直接登录。'); } $st = $pdo->prepare('INSERT INTO users (username, password_hash, created_at) VALUES (?,?,?)'); $st->execute([$u, password_hash($p, PASSWORD_DEFAULT), date('Y-m-d H:i:s')]); if ($siteName !== '') { $pdo->prepare('INSERT INTO settings (k,v) VALUES (?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v') ->execute(['site_name', $siteName]); } // 锁文件:写不进去不算致命(已有管理员账号时本页本来就拒绝执行) @file_put_contents($lockFile, date('c') . " installed\n"); if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } $_SESSION['uid'] = (int)$pdo->lastInsertId(); $done = true; } catch (Throwable $e) { $err = '安装失败:' . $e->getMessage(); } } } /* 装成功后统计一下库的信息(给用户看,也证明库真的建好了) */ $dbInfo = null; if ($done || $installed) { try { $pdo = db(); $cnt = static function (string $t) use ($pdo): int { return (int)$pdo->query("SELECT COUNT(*) FROM {$t}")->fetchColumn(); }; $dbInfo = [ 'path' => DB_PATH, 'size' => is_file(DB_PATH) ? (int)filesize(DB_PATH) : 0, 'groups' => $cnt('groups'), 'sites' => $cnt('sites'), 'engines' => $cnt('engines'), 'users' => $cnt('users'), ]; } catch (Throwable $e) { $dbInfo = null; } } $title = $installed ? '已安装' : '安装向导'; ?>
本系统已完成初始化,安装向导不会再执行任何写操作。
= e($dbInfo['path']) ?>
安全提示:请把服务器上的 install.php 删掉。
虽然已有管理员账号时本页会拒绝执行安装,但删掉它能少一个入口。
数据库已创建,管理员账号已就绪。
= e($dbInfo['path']) ?>
下一步(重要):把服务器上的 install.php 删掉。
后台默认已有一份示例板块和示例网站,进去之后可以随意删改、添加自己的链接。
本系统使用 SQLite:数据库是一个文件,不需要你填写数据库地址/账号/密码,向导会自动创建。
| = $ck['ok'] ? '✓' : ($ck['required'] ? '✕' : '!') ?> | = e($ck['name']) ?>= $ck['required'] ? '' : '(建议)' ?> | = e($ck['detail']) ?> |
chown -R www:www = e(APP_PATH) ?>/data = e(APP_PATH) ?>/uploads= e(DB_PATH) ?>
安装会创建数据表,并写入一份示例板块与示例网站作为起点(可在后台随意删改)。
装完请立刻删除服务器上的 install.php。