软件图标支持上传图片:存 OSS/本机,media.php 代理读取

用户要求图标可以自己上传一张真实图片,而不是只能填一个字符。

设计:字符和图片并存,图片优先
- software 加 icon_key(图片对象键),icon(字符)保留作兜底
- 有图片用图片,删掉图片自动退回字符,任何时刻都不会没有图标
- 前端 badge 与邮件头部同一套判断,页面和邮件永远一致

存储与读取
- 图标走独立键空间 icons/YYYY/MM/<16hex>.<ext>
  不复用附件那套:附件必须挂在 thread 下才给读,图标不属于任何帖子
- 写:仅管理员(后台动作链)+ finfo 真实类型校验 + 尺寸下限 + 2MB 上限
- 读:media.php 对 icons/ 只校验键形状与文件存在(图标本就公开),
  写入侧仍然严格,不会让本站变成能读整个桶的开放代理
- 优先用 media.php?k= 代理;OSS 下顺便借图片处理缩放,省下行

顺手修掉两个真实安全问题
1. fb_icon_key_ok 原来用 [a-z0-9]{2,5} 匹配扩展名,.php 正好能过
   (3 个字母)—— 测试反向验证时发现,已改为图片扩展名白名单
2. fb_upload_key_ok 有同样的问题,附件键也能构造出 .php
   虽然真上传还有 finfo 兜底,但键本身就不该长这样,一并收紧
3. fb_icon_delete 原来用 @ 吞掉错误,删除失败仍返回「已保存」,
   用户会以为旧图清了、实际留了个可访问的孤儿图。改为返回 bool 并如实提示

验证:键校验 7 类非法输入全拒;图标写入 OSS 成功、media.php 读回 200 image/png、
PNG 魔数正确、缩放生效;越权读取(穿越/.php/未入库附件/不存在)全部按预期拒绝;
表单 enctype/文件框/accept/预览渲染齐全;仿真 PHP 伪图与 SVG 均被拒;
16x16 小图被拒。
This commit is contained in:
2026-11-03 08:13:21 +08:00
parent 738c6c09ff
commit 835c8c05da
8 changed files with 377 additions and 21 deletions
+19 -5
View File
@@ -191,13 +191,27 @@ if (!function_exists('fb_mail_sw_header')) {
$icon = fb_software_icon($sw);
$name = (string)$sw['name'];
/* 有上传的图标图片就用图片(管理员自己的 logo),没有才用「色块 + 字符」。
* 邮件客户端对 flex 支持参差,这里一律用 table + 内联样式;
* 图片必须给死宽高和 alt,否则 Outlook 里会撑变形。 */
$imgKey = isset($sw['icon_key']) ? trim((string)$sw['icon_key']) : '';
$imgUrl = $imgKey !== '' ? fb_icon_url($imgKey, 60) : '';
$cell = '';
if ($imgUrl !== '') {
$cell = '<img src="' . fb_mail_esc($imgUrl) . '" alt="' . fb_mail_esc($name) . '"'
. ' width="30" height="30"'
. ' style="width:30px;height:30px;border-radius:8px;display:block;border:0;outline:none;text-decoration:none">';
} else {
$cell = '<div style="width:30px;height:30px;background:' . fb_mail_esc($c) . ';'
. 'border-radius:8px;text-align:center;line-height:30px;'
. 'font-size:15px;color:#ffffff;font-weight:700">'
. '<span style="display:inline-block;line-height:30px">' . fb_mail_esc($icon) . '</span></div>';
}
return '<table role="presentation" cellpadding="0" cellspacing="0" border="0"'
. ' style="border-collapse:collapse"><tr>'
. '<td width="30" style="width:30px;height:30px;background:' . fb_mail_esc($c) . ';'
. 'border-radius:8px;text-align:center;vertical-align:middle;'
. 'font-size:15px;line-height:30px;color:#ffffff;font-weight:700">'
. '<span style="display:inline-block;line-height:30px">' . fb_mail_esc($icon) . '</span>'
. '</td>'
. '<td width="30" style="width:30px;vertical-align:middle">' . $cell . '</td>'
. '<td style="padding-left:9px;vertical-align:middle;font-size:15px;'
. 'font-weight:700;color:#1b2430;letter-spacing:.2px">'
. fb_mail_esc($name) . '</td>'