修复: 评论组件泄露私密 token、cwebp 无超时、选项长度与去重
Waline(安全) - item.html 的 path 从 window.location.href 改为剔除 ?t= 后的 URL。 私密题 /_/<domain>/<id>?t=<token> 的钥匙原本会整条发往 Waline 服务端 并落进其访问日志。只删 t、保留其他参数,公开题的评论线程键不变。 cwebp(可靠性) - ConvertGifToAnimatedWebP 改用 exec.CommandContext + 30s 上限。 原来裸 exec.Command 无超时,畸形/超大 GIF 会把子进程和上传 goroutine 一起挂住。 超时杀进程并记 Warn,调用方按既有逻辑回退为原 GIF,不影响上传成功。 投票选项长度(正确性) - vote_options.content 由 varchar(200) 放宽到 300,与配置默认值一致。 此前 201–300 字的选项通过校验后在写库阶段失败。 - 新增 db.MaxVoteOptionLen;voteOptionMaxLen() 取配置与存储上限的较小值, 校验与前端 maxlength 同源,避免配置调大后再次越界。 多选 checkbox 去重(正确性) - 抽出 appendUniqueValue,预置值与「其他」文本共用同一 seen 集合。 此前重复提交同名字段会让同一选项在统计里被计多次。 测试 - vote_dedupe_test.go:去重与顺序。 - vote_option_len_test.go:配置小于/等于/大于存储上限三种情形。 - TestVoteOptionMaxLengthRoundTrip:300 个多字节字符存回不截断。 - TestVoteOptionColumnWidens:把列改回 200 后 AutoMigrate 能扩回 300(老库升级路径)。 验证:WSL2 Debian + PostgreSQL 全量 go test ./... 27 个包全绿; 隔离实例渲染单题页确认新的 path 逻辑。
This commit is contained in:
+19
-2
@@ -799,6 +799,19 @@ func parseMultipleOptionIDs(ids []string, maxChoices int, validOptions map[uint]
|
||||
return chosen, nil
|
||||
}
|
||||
|
||||
// appendUniqueValue 把 v 追加到 dst,但同一个值只保留第一次出现。
|
||||
// 多选类字段(checkbox)依赖它去重:客户端可以重复提交同名字段,
|
||||
// 不去重就会在结果统计里把同一选项计多次。
|
||||
//
|
||||
// seen 由调用方持有并跨多次调用复用,用来记录已追加过的值。
|
||||
func appendUniqueValue(dst []string, seen map[string]bool, v string) []string {
|
||||
if seen[v] {
|
||||
return dst
|
||||
}
|
||||
seen[v] = true
|
||||
return append(dst, v)
|
||||
}
|
||||
|
||||
// parseSubmission 按投票类型解析并校验提交内容,构造 VoteSubmission。
|
||||
// owner 为投票发起人(用于其全局屏蔽词表对开放文本的本地过滤)。
|
||||
func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.CreateVoteSubmissionOptions, error) {
|
||||
@@ -924,6 +937,10 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
|
||||
// 多选 checkbox
|
||||
if f.FieldType == db.VoteFormFieldTypeCheckbox {
|
||||
var chosen []string
|
||||
// 同一字段名可以被客户端重复提交(field_x=A&field_x=A),
|
||||
// 不去重就会在结果统计里把同一个选项计多次,等于让一个人刷高计数。
|
||||
// 按最终落库的字符串去重:预置值按值去重,「其他」按用户填的文本去重。
|
||||
chosenSeen := make(map[string]bool, len(values))
|
||||
for _, val := range values {
|
||||
val = strings.TrimSpace(val)
|
||||
if val == "" {
|
||||
@@ -947,13 +964,13 @@ func parseSubmission(ctx context.Context, v *db.Vote, owner *db.User) (*db.Creat
|
||||
if err := censorCheck(ctx, owner, lang, custom); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chosen = append(chosen, custom)
|
||||
chosen = appendUniqueValue(chosen, chosenSeen, custom)
|
||||
continue
|
||||
}
|
||||
if !validFieldOptions[val] {
|
||||
return nil, errors.New(i18n.T(lang, "vote.err.fieldInvalidOption", f.Label))
|
||||
}
|
||||
chosen = append(chosen, val)
|
||||
chosen = appendUniqueValue(chosen, chosenSeen, val)
|
||||
}
|
||||
if len(chosen) == 0 && f.Required {
|
||||
return nil, errors.New(i18n.T(lang, "vote.err.fieldRequired", f.Label))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package vote
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAppendUniqueValue 钉住多选字段的去重语义:
|
||||
// 客户端重复提交同一选项时,落库与统计都只能算一次。
|
||||
func TestAppendUniqueValue(t *testing.T) {
|
||||
t.Run("重复值只保留第一次", func(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
var got []string
|
||||
for _, v := range []string{"A", "A", "B", "A", "B", "C"} {
|
||||
got = appendUniqueValue(got, seen, v)
|
||||
}
|
||||
want := []string{"A", "B", "C"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("got %v, want %v(顺序应按首次出现)", got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("空切片与单值", func(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
var got []string
|
||||
got = appendUniqueValue(got, seen, "只有一项")
|
||||
if len(got) != 1 || got[0] != "只有一项" {
|
||||
t.Fatalf("got %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("seen 复用时跨批次仍然去重", func(t *testing.T) {
|
||||
// checkbox 的「其他」文本与预置值走同一条追加路径,
|
||||
// seen 必须在整个字段解析期间共享,而不是每轮新建。
|
||||
seen := map[string]bool{}
|
||||
var got []string
|
||||
got = appendUniqueValue(got, seen, "预置")
|
||||
got = appendUniqueValue(got, seen, "其他文本")
|
||||
got = appendUniqueValue(got, seen, "预置")
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %v, want 2 项", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user