修设置页主题色块被模板过滤器替换成 ZgotmplZ

html/template 对 style 属性做 CSS 过滤,linear-gradient(...) 未标记安全会被整体替换成
ZgotmplZ —— 线上设置页四个主题色块一直是灰底(既有问题,不是这次配色改动引入)。

- FuncMap 新增 safeCSS(string → template.CSS);值只来自 internal/theme.swatchOf,
  由两个已校验的色值拼成,不接受用户输入,所以这里放开是安全的
- user/profile.html 的色块改 {{ safeCSS .Swatch }}
- 新增 TestThemeSwatchRenders:渲染 user/profile,断言不含 ZgotmplZ 且含渐变串;
  已做变异验证(去掉 safeCSS 后该用例失败)
This commit is contained in:
tamakyi
2026-10-21 18:53:10 +08:00
parent 27856dd4a2
commit 1f29d3eb2e
4 changed files with 47 additions and 1 deletions
+16
View File
@@ -496,3 +496,19 @@ shiroko `#4a7fc1`(3.69) → **`#3b6dac`**(4.74),sakura `#e5678f`(2.80) → **`
`_scaffold.css` 令牌数 29 → 30。`StaticVersion` → **`20261021c`**。 `_scaffold.css` 令牌数 29 → 30。`StaticVersion` → **`20261021c`**。
最终审计 8 种组合(base 亮暗 + 三主题亮暗)全部 ≥4.5。⚠️ 写 CSS 用 python 改时注意:`git checkout` 会把 最终审计 8 种组合(base 亮暗 + 三主题亮暗)全部 ≥4.5。⚠️ 写 CSS 用 python 改时注意:`git checkout` 会把
base.css 还原成 CRLF,插入行要用同一行尾,否则变 MIXED。 base.css 还原成 CRLF,插入行要用同一行尾,否则变 MIXED。
## 18:5x 检查测试环境部署 → 抓到设置页色块 ZgotmplZ
**部署核对(都对)**:`base.css?v=20261021c`、三份 CSS(base/sakura/shiroko)与本地**逐字节一致**;
设置页主题列表 = 跟随站点默认 / 基础外观 / 樱花 / 砂狼白子,**test01 已消失**、名称无 `theme.name.*` key;
登录按钮是新蓝(#0e6fd0)白字,箱主页排版正常。
⚠️ **admin6 的主题被重置成「跟随站点默认」**(它原来选的是已删除的 test01)→ 箱主页 `<html>` 没有
`data-theme-style`,现在是基础外观。要验证主题得重新选 sakura/shiroko。
**P1 缺陷(既有,非本次改动引入)**:设置页四个主题色块全是 `style="background: ZgotmplZ"`。
`html/template` 会对 `style` 属性做 CSS 过滤,未标记安全的 `linear-gradient(...)` 一律替换成 ZgotmplZ。
`swatchOf` 一直在生成渐变串,所以这个色块**从来没显示过**。
修法:FuncMap 加 `safeCSS` → `template.CSS`,模板 `{{ safeCSS .Swatch }}`(值只来自 swatchOf,不接受用户输入)。
加了 `TestThemeSwatchRenders`(渲染 user/profile,断言不含 ZgotmplZ 且含渐变串),并做变异验证:
把 `safeCSS` 去掉 → 测试如期 FAIL。
⚠️ 教训:任何 `style="..."` 里塞模板值都要过一遍这个过滤器;模板名在测试里**不带 `.html`**(`user/profile`)。
+7
View File
@@ -199,6 +199,13 @@ func FuncMap() []template.FuncMap {
"themeName": func(lang, slug string) string { "themeName": func(lang, slug string) string {
return theme.DisplayName(lang, slug) return theme.DisplayName(lang, slug)
}, },
// safeCSS 把受控的 CSS 值标成安全,用于主题色块的 inline background。
// ⚠️ html/template 会过滤 style 属性里的值,linear-gradient(...) 直接被替换成
// ZgotmplZ —— 设置页每个主题的色块都会变成灰底(线上就是这样)。
// 值只来自 internal/theme.swatchOf(由两个已校验的色值拼成),不接受用户输入。
"safeCSS": func(s string) template.CSS {
return template.CSS(s)
},
// moderationStrictness 返回账号「最终生效」的审核严格度档位(lax / standard / strict)。 // moderationStrictness 返回账号「最终生效」的审核严格度档位(lax / standard / strict)。
// //
+23
View File
@@ -17,6 +17,7 @@ import (
"github.com/tamakyi/TamaBox/internal/i18n" "github.com/tamakyi/TamaBox/internal/i18n"
"github.com/tamakyi/TamaBox/internal/security/moderation" "github.com/tamakyi/TamaBox/internal/security/moderation"
templatepkg "github.com/tamakyi/TamaBox/internal/template" templatepkg "github.com/tamakyi/TamaBox/internal/template"
"github.com/tamakyi/TamaBox/internal/theme"
) )
// 审核严格度下拉框的解析:不依赖属性的书写顺序或是否存在额外属性 // 审核严格度下拉框的解析:不依赖属性的书写顺序或是否存在额外属性
@@ -423,3 +424,25 @@ func TestRenderUserQuestionList(t *testing.T) {
t.Fatal("restriction banner should show expiry date") t.Fatal("restriction banner should show expiry date")
} }
} }
// TestThemeSwatchRenders 主题色块走的是 inline style="background: <gradient>"。
// html/template 对 style 属性做 CSS 过滤,未标记安全的 linear-gradient(...) 会被换成
// ZgotmplZ —— 设置页每个主题的色块都变成灰底。这个用例钉住「色块真的渲染出来」。
func TestThemeSwatchRenders(t *testing.T) {
base := profileBaseData()
base["Themes"] = []theme.Theme{
{Slug: "sakura", Swatch: "linear-gradient(135deg, #fceef3 0 45%, #ba3e66 45% 100%)"},
}
base["CurrentTheme"] = "sakura"
out, err := renderToString(t, "user/profile", base)
if err != nil {
t.Fatalf("render: %v", err)
}
if strings.Contains(out, "ZgotmplZ") {
t.Fatal("色块被 html/template 过滤成 ZgotmplZ(检查是否漏了 safeCSS)")
}
if !strings.Contains(out, "linear-gradient(135deg, #fceef3 0 45%, #ba3e66 45% 100%)") {
t.Fatal("渲染结果里找不到主题色块的渐变")
}
}
+1 -1
View File
@@ -804,7 +804,7 @@
{{ range .Themes }} {{ range .Themes }}
<label class="tb-theme-opt"> <label class="tb-theme-opt">
<input class="uk-radio" type="radio" name="theme" value="{{ .Slug }}"{{ if eq $.CurrentTheme .Slug }} checked{{ end }}> <input class="uk-radio" type="radio" name="theme" value="{{ .Slug }}"{{ if eq $.CurrentTheme .Slug }} checked{{ end }}>
<span class="tb-theme-swatch"{{ if .Swatch }} style="background: {{ .Swatch }}"{{ end }}></span> <span class="tb-theme-swatch"{{ if .Swatch }} style="background: {{ safeCSS .Swatch }}"{{ end }}></span>
<span class="tb-theme-name">{{ themeName $.LangCode .Slug }}</span> <span class="tb-theme-name">{{ themeName $.LangCode .Slug }}</span>
</label> </label>
{{ end }} {{ end }}