Files
FeedLite/lib/bootstrap.php
T
laoyang f1a81cbaa1 RAM 角色自动探测 + 邮件配置界面 + 每个软件独立主题色 (#1 #2)
## 1. ECS RAM 角色改为自动探测
原先要用户去阿里云控制台查「实例 RAM 角色名」再手填,纯属多余 ——
机器自己知道答案。

新增 fb_oss_detect_instance_role():读元数据
GET /latest/meta-data/ram/security-credentials/(末尾带斜杠)拿到角色名列表。
- 兼容单行 / 多行 / JSON 数组三种返回
- 未配置角色时 fb_oss_instance_creds() 自动探测,不再报「未配置」
- 后台改成只读展示「已绑定:xxx」,手填入口收进「高级」折叠里
- 保存时留空 = 自动探测后写入,不存空值
- 探测失败给出可操作原因(没绑角色 / 不在 ECS 上 / 元数据被禁用)

## 2. 邮件配置界面(原先完全缺失)
lib/mailer.php 有完整 SMTP 实现,但后台一个输入框都没有,
只能改 config.php —— 用户装完根本找不到地方配。

根因有两层:
- fb_setting_keys() 白名单里没有任何 smtp.* / notify.* 键,
  就算加了表单,fb_set_setting() 也会把值全部丢弃
- 设置页只有「站点信息」「附件存储」两个区块

补上:白名单 15 个键 + 后台「邮件通知」区块
- SMTP 服务器/端口/加密方式(SSL/STARTTLS/不加密)
- 登录账号/密码(留空不修改,避免每次重填)
- 发件地址/显示名、超时、SSL 校验
- 通知规则:回复通知、每次回复都通知、新反馈通知管理员、发信冷却
- 「发送测试邮件」按钮,AJAX 发完就地显示成功或失败原因,
  不用保存再靠猜

## 3. 每个软件一个主题色
需求:从短链接进不同软件,颜色不同,一眼区分。

- software 表加 color 字段(幂等迁移)
- 全站配色靠 --brand / --brand-600 / --brand-soft 三个变量,
  按软件覆写这三个即可,不用为每个软件写一套 CSS
- fb_color_variants() 由主色推出深一档(×0.85)与浅一档(往白混 88%)
- 未配色的软件按 id 从 8 色调色板取默认色,保证同站不撞色
- 后台:颜色选择器(原生取色 + 8 个预设 + 用默认)
- 前台:index / new / thread 都按当前软件注入主题色,
  meta theme-color 也跟着变

验证:三功能专项 30/30;新旧回归全过
(全新安装 33/33、老基线 25/25、附件 22/22、安装向导 60/60、约束检查 20/20)
2026-11-01 16:39:09 +08:00

146 lines
6.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 启动引导:加载配置、注册自动加载、开启会话、初始化数据库。
* 所有页面(index/thread/new/api)都以此为入口。
*/
if (!defined('FB_ROOT')) {
define('FB_ROOT', dirname(__DIR__));
// 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1,
// 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。
if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 13); }
}
/* ------------------------------------------------------------------
* 错误显示策略必须最先设置:
* 任何被打印到页面的 Warning/Deprecated 都会让响应头提前发出,
* 从而令随后的 session_start() 失败(表现为「会话已过期」、CSRF 全部失效)。
* 所以这里在 require 任何库文件之前就把 display_errors 关掉。
* ------------------------------------------------------------------ */
@ini_set('display_errors', '0');
@ini_set('log_errors', '1');
error_reporting(E_ALL);
// 兜底:捕获致命错误时也不要向页面输出堆栈(生产环境)
if (!defined('FB_BOOTSTRAPPED')) {
define('FB_BOOTSTRAPPED', 1);
}
// 后台改设置后自增,用来让 fb_cfg 的进程内缓存失效
if (!isset($GLOBALS['__fb_cfg_stamp'])) {
$GLOBALS['__fb_cfg_stamp'] = 0;
}
if (!function_exists('fb_cfg')) {
/**
* 只读【配置文件】的值(config.php → config.example.php),
* **不叠加 settings 表、绝不触达数据库**。
*
* 为什么必须单独有这么一条通道:正常的 fb_cfg() 会去读 settings 表,
* 而读表要先 fb_db() —— 那一步会就地建出一个空数据库文件。
* 「是否已安装」的判定恰恰以「数据库文件在不在」为依据,
* 所以判定必须走这条不碰库的路径,否则第一秒就把安装态自己造出来了。
*/
function fb_cfg_file() {
static $file = null;
static $stamp = -1;
$now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0;
if ($file === null || $stamp !== $now) {
$local = FB_ROOT . '/config.php';
$example = FB_ROOT . '/config.example.php';
if (is_file($local)) { $file = (array)(require $local); }
elseif (is_file($example)) { $file = (array)(require $example); }
else { $file = []; }
$stamp = $now;
}
return $file;
}
/**
* 读取配置(点分路径,如 fb_cfg('smtp.host'))
* 取值顺序:settings 表(后台改过的)> config.php(本地配置)> config.example.php(出厂默认)> $default
*
* config.php 不参与版本管理(见 .gitignore),新部署时可从 config.example.php 复制一份;
* 若不存在则直接以 config.example.php 为默认值运行,保证 clone 下来即可跑。
*/
function fb_cfg($path = null, $default = null) {
static $merged = null;
static $stamp = -1;
$now = isset($GLOBALS['__fb_cfg_stamp']) ? (int)$GLOBALS['__fb_cfg_stamp'] : 0;
if ($merged === null || $stamp !== $now) {
$cfg = fb_cfg_file();
// 后台设置以嵌套数组形式叠加在文件配置之上(只覆盖同名键)
$tree = function_exists('fb_setting_tree') ? fb_setting_tree() : [];
$merged = is_array($tree) && $tree ? array_replace_recursive($cfg, $tree) : $cfg;
$stamp = $now;
}
if ($path === null || $path === '') { return $merged; }
$cur = $merged;
foreach (explode('.', (string)$path) as $k) {
if (is_array($cur) && array_key_exists($k, $cur)) {
$cur = $cur[$k];
} else {
return $default;
}
}
return $cur;
}
}
/** 让 fb_cfg() 的进程内缓存失效(改了磁盘上的 config.php 后必须调用) */
function fb_cfg_flush() {
$GLOBALS['__fb_cfg_stamp'] = ((int)$GLOBALS['__fb_cfg_stamp']) + 1;
}
require_once FB_ROOT . '/lib/util.php';
require_once FB_ROOT . '/lib/db.php';
require_once FB_ROOT . '/lib/oss.php';
require_once FB_ROOT . '/lib/attach.php';
require_once FB_ROOT . '/lib/captcha.php';
require_once FB_ROOT . '/lib/mailer.php';
require_once FB_ROOT . '/lib/layout.php';
/* ------------------------------------------------------------------
* 安装引导:数据库还没生成(data/ 下没数据)就说明是全新部署,
* 一律先送去 install.php 填配置、建库、设口令,装完才进系统。
* 判据只看「数据库文件在不在」——绝不能在这里调 fb_db(),
* 因为那会就地建出一个空库、把安装态自己"装"出来,之后就再也进不了向导。
* ------------------------------------------------------------------ */
if (!defined('FB_SKIP_INSTALL_REDIRECT') && !fb_installed()) {
if (PHP_SAPI === 'cli') {
fwrite(STDERR, "FeedLite 尚未安装:请先在浏览器中打开本目录下的 install.php 完成安装。\n");
exit(1);
}
fb_redirect('install.php');
}
// 会话(CLI 下不启用)
if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {
$p = session_get_cookie_params();
$https = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off');
@session_set_cookie_params([
'lifetime' => 0,
'path' => !empty($p['path']) ? $p['path'] : '/',
'domain' => '',
'secure' => $https,
'httponly' => true,
'samesite' => 'Lax',
]);
@session_start();
}
// 初始化数据库(建表)。
// ★ 只在【已安装】时做:未安装就建库,等于第一秒把这个状态自己抹掉,
// 安装向导(install.php)随后就会被判定成「已经装好了」而拒绝工作。
// install.php 在真正提交安装时会自己显式调用 fb_db()。
if (fb_installed()) {
fb_db();
// 顺手确保 uploads/ 目录与「禁止直读」规则在位且为最新版(local / oss 都需要)。
// ⚠️ 为什么放在这里:老站点上早已存在的旧版 .htaccess(曾允许直读)不会自己更新,
// 而部署账号通常对 uploads/ 无写权限 —— 只有 PHP(目录属主)能改。
// 函数内部只在「文件缺失或版本标记过期」时才写盘,命中后每次请求几乎零开销。
if (function_exists('fb_local_ensure_dir')) {
fb_local_ensure_dir();
}
}