修复:图标存储未跟随后台配置,本地模式下图标显示不出来
用户指出图标应当与附件同一套逻辑:管理员在后台配 local 就存本机、 配 oss 就存 OSS,前台都一样走 media.php 代理。 原实现把图标键硬编码成 icons/…(不带存储前缀),导致: - fb_attach_is_local() 恒为 false - 本机模式下文件确实写进了 uploads/icons/, 但 media.php 把它当 OSS 对象去读 → 503,图标完全显示不出来 修法:图标键与附件完全同规则,只是路径里多一段 icons/ local 模式 → local/icons/YYYY/MM/<hex>.<ext> 落 uploads/icons/ oss 模式 → <oss.prefix>/icons/YYYY/MM/<hex>.<ext> 落私有桶 前台地址两种模式一致(media.php?k=…),换存储方式旧图标不会失效。 同时: - fb_icon_is_key/ok 改为同时识别两种前缀 - fb_icon_allowed_ext 移到 attach.php(media.php 与校验都会用, 不能依赖 admin_taxonomy.php 被加载) - 写入时加断言:键前缀必须与当前存储模式一致,脱钩就报错, 不再静默存到错误位置 验证(线上真实环境,两种模式各跑一遍): - local:键带 local/icons/ 前缀、写入 uploads/icons/ 成功、 media.php 读回 200 image/png 501B、PNG 魔数正确、删除后 404 - oss:键带 fb/icons/ 前缀、is_local=false、URL 拼接正确 - 旧 bug 形态(icons/… 无前缀)现在被明确拒绝 400,不再误判成 OSS - 测试后配置已复原为 oss,临时文件与诊断入口全部清理
This commit is contained in:
+12
-6
@@ -19,12 +19,8 @@ if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
|
||||
* 对外一律 media.php?k=... 代理,不暴露真实位置。
|
||||
* ================================================================== */
|
||||
|
||||
/** 图标允许的扩展名(比附件白名单窄:图标不需要 gif 动图,也不要 svg) */
|
||||
function fb_icon_allowed_ext() {
|
||||
// ⚠️ 刻意不收 svg:SVG 能内嵌 <script>,一旦被当图片直出到浏览器就是 XSS。
|
||||
// 图标是管理员自己传的,但防线不该依赖「管理员不会传坏东西」。
|
||||
return ['png', 'jpg', 'jpeg', 'webp'];
|
||||
}
|
||||
/** 图标允许的扩展名 —— 定义在 lib/attach.php(fb_icon_allowed_ext),
|
||||
* 因为 media.php 与 CLI 场景也会用到,不能依赖这个后台文件被加载。 */
|
||||
|
||||
/**
|
||||
* 保存一个上传的图标文件,返回 [bool ok, string 值(键或错误信息), string mime]。
|
||||
@@ -77,6 +73,16 @@ function fb_icon_store_upload(array $file) {
|
||||
|
||||
$key = fb_icon_make_key($ext);
|
||||
|
||||
/* 断言:键的前缀必须与当前存储方式一致。
|
||||
* 这两者一旦脱钩,就会出现「文件写进 uploads/ 但 media.php 去 OSS 找」的 503 ——
|
||||
* 早期把键硬编码成 icons/… 就是这么坏的。宁可当场报错,也不要静默存错位置。 */
|
||||
if ($mode === 'local' && !fb_attach_is_local($key)) {
|
||||
return [false, '内部错误:本地模式下生成了非本机路径的图标键。', ''];
|
||||
}
|
||||
if ($mode === 'oss' && fb_attach_is_local($key)) {
|
||||
return [false, '内部错误:OSS 模式下生成了本机路径的图标键。', ''];
|
||||
}
|
||||
|
||||
if ($mode === 'local') {
|
||||
if (!fb_local_writable()) { return [false, '本机附件目录不可写(uploads/),无法保存图标。', '']; }
|
||||
$path = fb_local_path_of($key);
|
||||
|
||||
+48
-16
@@ -152,34 +152,63 @@ function fb_attach_is_local($key) {
|
||||
/* ==================================================================
|
||||
* 软件图标(管理员上传的图片)
|
||||
* ------------------------------------------------------------------
|
||||
* 🔴 存储位置【跟随后台配置】,与附件完全同一套规则:
|
||||
* attachment.storage = local → local/icons/YYYY/MM/<hex>.<ext> 写 uploads/
|
||||
* attachment.storage = oss → <oss.prefix>/icons/YYYY/MM/<hex>.<ext> 写私有桶
|
||||
* 管理员在后台配的哪种,图标就存哪种;前台地址两种模式完全一致,
|
||||
* 都是 media.php?k=... 代理,换存储方式不会让已有图标失效。
|
||||
*
|
||||
* 早期实现把图标键固定写成 `icons/...`(不带 local/ 前缀),
|
||||
* 结果 fb_attach_is_local() 永远返回 false —— 本机模式下文件明明写进了
|
||||
* uploads/icons/,media.php 却当成 OSS 对象去读,直接 503。
|
||||
* 这是「本地存储时图标显示不出来」的根因,键必须带存储前缀。
|
||||
*
|
||||
* 为什么不复用普通附件:
|
||||
* 普通附件必须挂在某条 thread 下才允许被 media.php 读出(否则本站就成了
|
||||
* 「能读整个桶」的开放代理)。软件图标不属于任何帖子,用它那套鉴权会被拒。
|
||||
*
|
||||
* 于是单独给一个键空间 `icons/`,并且:
|
||||
* ① 只有管理员能写入(后台动作 + 登录校验);
|
||||
* ② media.php 对 `icons/` 前缀只做「合法键 + 文件存在」判断,不做帖子鉴权;
|
||||
* ③ 键的形状强约束成 icons/YYYY/MM/<16hex>.<ext>,无法穿越、无法指向别处。
|
||||
* 图标本来就是公开信息(每个访客都能看到),不需要保密,只需要防写入。
|
||||
*/
|
||||
* 于是键里多一段 `icons/`,media.php 据此识别并按公开资源处理(写入仍受控)。
|
||||
* ================================================================== */
|
||||
function fb_icon_key_prefix() {
|
||||
return 'icons/';
|
||||
}
|
||||
|
||||
/** 这个键是不是软件图标 */
|
||||
function fb_icon_is_key($key) {
|
||||
return strpos((string)$key, fb_icon_key_prefix()) === 0;
|
||||
/**
|
||||
* 图标允许的扩展名(比附件白名单窄:图标不需要 gif 动图)。
|
||||
* ⚠️ 刻意不收 svg:SVG 能内嵌 <script>,一旦被当图片直出到浏览器就是 XSS。
|
||||
* 图标是管理员自己传的,但防线不该依赖「管理员不会传坏东西」。
|
||||
*
|
||||
* 定义在这里而不是 admin_taxonomy.php:media.php 与校验函数都会用到,
|
||||
* 不能依赖后台那个文件被加载(它在 bootstrap 里并不 require)。
|
||||
*/
|
||||
function fb_icon_allowed_ext() {
|
||||
return ['png', 'jpg', 'jpeg', 'webp'];
|
||||
}
|
||||
|
||||
/** 生成一个图标对象键(始终为本机前缀,OSS 模式在写入时换前缀) */
|
||||
/** 这个键是不是软件图标(local/icons/… 或 <prefix>/icons/…) */
|
||||
function fb_icon_is_key($key) {
|
||||
return (bool)preg_match('#^(?:local|[A-Za-z0-9][A-Za-z0-9._-]*)/icons/#', (string)$key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成一个图标对象键 —— 存储位置由后台配置决定,与附件一致。
|
||||
* local 模式带 `local/` 前缀,OSS 模式带 oss.prefix 前缀;
|
||||
* 两种都能被 fb_attach_is_local() / fb_local_path_of() 正确解析。
|
||||
*/
|
||||
function fb_icon_make_key($ext) {
|
||||
$ext = strtolower(preg_replace('/[^A-Za-z0-9]/', '', (string)$ext));
|
||||
return fb_icon_key_prefix() . date('Y/m') . '/' . bin2hex(random_bytes(8)) . '.' . $ext;
|
||||
$ext = strtolower(preg_replace('/[^A-Za-z0-9]/', '', (string)$ext));
|
||||
$leaf = fb_icon_key_prefix() . date('Y/m') . '/' . bin2hex(random_bytes(8)) . '.' . $ext;
|
||||
|
||||
if (fb_store_mode() === 'local') {
|
||||
return 'local/' . $leaf;
|
||||
}
|
||||
$prefix = trim((string)fb_cfg('oss.prefix', 'fb'), '/');
|
||||
if ($prefix === '') { $prefix = 'fb'; }
|
||||
return $prefix . '/' . $leaf;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标键是否合法。
|
||||
* 与 fb_upload_key_ok 分开写:图标键不落在 local/ 也不落 oss.prefix 下,
|
||||
* 与 fb_upload_key_ok 分开写:图标键在附件键的基础路径里多一段 icons/,
|
||||
* 塞进原函数会被判为不合法。
|
||||
*
|
||||
* 🔴 扩展名必须是白名单里的图片类型,不能只写 `[a-z0-9]{2,5}` ——
|
||||
@@ -190,13 +219,16 @@ function fb_icon_key_ok($key) {
|
||||
$key = (string)$key;
|
||||
if (strlen($key) < 16 || strlen($key) > 200) { return false; }
|
||||
if (strpos($key, '..') !== false || strpos($key, '//') !== false) { return false; }
|
||||
if (!preg_match('#^icons/\d{4}/\d{2}/[a-f0-9]{16}\.([a-z0-9]{2,5})$#', $key, $m)) { return false; }
|
||||
return in_array(strtolower($m[1]), ['png', 'jpg', 'jpeg', 'webp'], true);
|
||||
if (!preg_match('#^(?:local|[A-Za-z0-9][A-Za-z0-9._-]*)/icons/\d{4}/\d{2}/[a-f0-9]{16}\.([a-z0-9]{2,5})$#', $key, $m)) {
|
||||
return false;
|
||||
}
|
||||
return in_array(strtolower($m[1]), fb_icon_allowed_ext(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图标图片的【对外地址】—— 一律走 media.php 代理。
|
||||
* 与附件完全同一套约定:不暴露 uploads/ 真实路径,也不暴露任何 OSS 痕迹。
|
||||
* 两种存储模式对外都是这一个形态,所以换存储方式不影响已显示的图标。
|
||||
*
|
||||
* @param string $key 图标对象键
|
||||
* @param int $w 期望宽度(0=原图)。OSS 模式下交给服务端缩放;本机模式忽略。
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ if (!defined('FB_ROOT')) {
|
||||
define('FB_ROOT', dirname(__DIR__));
|
||||
// 静态资源版本号:改 assets/ 下的 css/js 后把这个数字 +1,
|
||||
// 让浏览器与 CDN 缓存失效。全站只此一处,别再往页面里写死数字。
|
||||
if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 18); }
|
||||
if (!defined('FB_ASSET_V')) { define('FB_ASSET_V', 19); }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user