feat: 外观支持自定义 CSS/JS,并升级发帖工作台与积分存量看板
后台可注入全站 CSS/JS(SSR 生效,CSS 可热换);发帖/编辑改为锁一屏工作室;经济看板增加存量健康度;主题令牌与弹层误关一并收紧。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
19
backend/service/custom_code_settings_test.go
Normal file
19
backend/service/custom_code_settings_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeCustomSnippet(t *testing.T) {
|
||||
got, err := sanitizeCustomSnippet(" body{color:red} ", "</style", MaxCustomCSSRunes)
|
||||
if err != nil || got != "body{color:red}" {
|
||||
t.Fatalf("trim got %q err=%v", got, err)
|
||||
}
|
||||
if _, err = sanitizeCustomSnippet("a</style>b", "</style", MaxCustomCSSRunes); err == nil {
|
||||
t.Fatal("css closer should fail")
|
||||
}
|
||||
if _, err = sanitizeCustomSnippet("a</SCRIPT>b", "</script", MaxCustomJSRunes); err == nil {
|
||||
t.Fatal("js closer should fail")
|
||||
}
|
||||
if _, err = sanitizeCustomSnippet("ok", "</script", MaxCustomJSRunes); err != nil {
|
||||
t.Fatalf("valid js: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,10 @@ const (
|
||||
SettingKeyAttachmentMaxCount = "attachment_max_count"
|
||||
// SettingKeyImageMaxMB 正文插图上限(MB);缺行=5
|
||||
SettingKeyImageMaxMB = "image_max_mb"
|
||||
// SettingKeyCustomCSS 全站自定义 CSS(外观页注入,空=无)
|
||||
SettingKeyCustomCSS = "custom_css"
|
||||
// SettingKeyCustomJS 全站自定义 JS(外观页注入,空=无)
|
||||
SettingKeyCustomJS = "custom_js"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -70,6 +74,9 @@ const (
|
||||
MinAttachmentMaxCount = 1
|
||||
MaxAttachmentMaxCount = 20
|
||||
MaxAttachmentExtCount = 80
|
||||
|
||||
MaxCustomCSSRunes = 48000
|
||||
MaxCustomJSRunes = 48000
|
||||
)
|
||||
|
||||
// DefaultAttachmentExts 论坛向默认允许扩展名(含安装包/脚本/网页)
|
||||
@@ -115,6 +122,8 @@ type PublicSiteSettings struct {
|
||||
AttachmentMaxMB int `json:"attachment_max_mb"`
|
||||
AttachmentMaxCount int `json:"attachment_max_count"`
|
||||
ImageMaxMB int `json:"image_max_mb"`
|
||||
CustomCSS string `json:"custom_css"`
|
||||
CustomJS string `json:"custom_js"`
|
||||
}
|
||||
|
||||
// SettingService 站点级键值设置
|
||||
@@ -316,6 +325,18 @@ func (s *SettingService) Public() (PublicSiteSettings, error) {
|
||||
return out, err
|
||||
}
|
||||
out.ImageMaxMB = imgMB
|
||||
|
||||
css, err := s.CustomCSS()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.CustomCSS = css
|
||||
|
||||
js, err := s.CustomJS()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.CustomJS = js
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -767,3 +788,65 @@ func (s *SettingService) ImageMaxBytes() (int64, error) {
|
||||
}
|
||||
return int64(mb) << 20, nil
|
||||
}
|
||||
|
||||
func sanitizeCustomSnippet(s, closer string, maxRunes int) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.ContainsRune(s, 0) {
|
||||
return "", ErrInvalidSiteSetting
|
||||
}
|
||||
if utf8.RuneCountInString(s) > maxRunes {
|
||||
return "", ErrInvalidSiteSetting
|
||||
}
|
||||
if closer != "" && strings.Contains(strings.ToLower(s), closer) {
|
||||
return "", ErrInvalidSiteSetting
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// CustomCSS 全站自定义 CSS。缺行/空=无。
|
||||
func (s *SettingService) CustomCSS() (string, error) {
|
||||
v, found, err := s.getValue(SettingKeyCustomCSS)
|
||||
if err != nil || !found {
|
||||
return "", err
|
||||
}
|
||||
out, nerr := sanitizeCustomSnippet(v, "</style", MaxCustomCSSRunes)
|
||||
if nerr != nil {
|
||||
return "", nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetCustomCSS(css string) error {
|
||||
normalized, err := sanitizeCustomSnippet(css, "</style", MaxCustomCSSRunes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalized == "" {
|
||||
return s.deleteKey(SettingKeyCustomCSS)
|
||||
}
|
||||
return s.putValue(SettingKeyCustomCSS, normalized)
|
||||
}
|
||||
|
||||
// CustomJS 全站自定义 JS。缺行/空=无。
|
||||
func (s *SettingService) CustomJS() (string, error) {
|
||||
v, found, err := s.getValue(SettingKeyCustomJS)
|
||||
if err != nil || !found {
|
||||
return "", err
|
||||
}
|
||||
out, nerr := sanitizeCustomSnippet(v, "</script", MaxCustomJSRunes)
|
||||
if nerr != nil {
|
||||
return "", nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetCustomJS(js string) error {
|
||||
normalized, err := sanitizeCustomSnippet(js, "</script", MaxCustomJSRunes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalized == "" {
|
||||
return s.deleteKey(SettingKeyCustomJS)
|
||||
}
|
||||
return s.putValue(SettingKeyCustomJS, normalized)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user